程序員進階之算法練習(七)

前言

最近來公司面試的開發者很多,經驗從1、2年,到5、6年都有,大都不堪重用。
或許在一些程序員眼里,能實現功能,保證上線即可。代碼質量,可擴展性,復雜度估計,都無所謂。
簡書上也很多人問我某個功能怎么實現,某個功能會導致手機發燙,如何優化。
而這些,就是為什么要進行算法訓練的原因。
算法練習帶來的代碼功力可以輕松駕馭各種功能的實現,對時間和空間的嚴格限制讓我們必須對代碼進行優化。同時做完每道題,都要通過大量的數據測試,代碼存在瑕疵很難通過最后的數據。
不是說每個優秀的程序員都要進行算法訓練,而是進行算法訓練會讓自己變得更加優秀。

這次有五道題:

  • A題是普通的實現題;
  • B題可以各憑本事實現;
  • C題是一個簡單的動態規劃;
  • D題可以用二分,也可以用字典樹;
  • E題考驗代碼實現功底;

看完題目大意,先思考,再看解析;覺得題目大意不清晰,點擊題目鏈接看原文。

文集:
程序員進階之算法練習(一)
程序員進階之算法練習(二)
程序員進階之算法練習(三)
程序員進階之算法練習(四)
程序員進階之算法練習(五)
程序員進階之算法練習(六)
代碼地址

A

題目鏈接
題目大意:點A在(a,b),n個點正向A靠近,求最短時間。

輸入:
第一行 a b ,表示點A的坐標(a, b);a and b (?-?100?≤?a,?b?≤?100)
第二行 n ,表示n個點;n (1?≤?n?≤?1000)
接下來n行,每行 x y v, 分別表示點坐標(x, y)和速度v。 xi, yi and vi (?-?100?≤?xi,?yi?≤?100, 1?≤?vi?≤?100)

輸出:
最短的等待時間,精確到10^-6。

Examples
input
0 0
2
2 0 1
0 2 2
output
1.00000000000000000000

代碼實現

    double minTime = 0x1.fffffep+127f;
    while (n--) {
        int x, y, v;
        cin >> x >> y >> v;
        minTime = min(minTime, sqrt((x- a) * (x - a) + (y - b) * (y - b)) / v);
    }
    printf("%.7f", minTime);

題目解析
根據題意,分別計算每個點與A的距離,除以對應的速度v得到到達時間,從中取最小時間。

B

題目鏈接
題目大意
n個數,q個詢問,對于每個詢問m[i],輸出n個數中小于等于m[i]的數量;

輸入:
第一行 n的范圍 (1?≤?n?≤?100?000)
第二行 n個數,x[i] (1?≤?x[i]?≤?100?000)
第三行 q的范圍, (1?≤?q?≤?100?000)
接下來q行,每行m[i] (1?≤?m[i]?≤?109)
輸出:
對于每一個m[i],輸出n個數中小于等于m[i]的數量;

Example
input
5
3 10 8 6 11
4
1
10
3
11
output
0
4
1
5

代碼實現

    for (int i = 0; i < n; ++i) {
        int k;
        cin >> k;
        ++a[k];
    }

     for (int i = 1; i < N; ++i) {
        dp[i] = a[i] + dp[i - 1];
    }
    
    int q;
    cin >> q;
    while (q--) {
        int k;
        cin >> k;
        k = min(k, N - 1);
        cout << dp[k] << endl;
    }

題目解析
方法多種多樣:
1、排序,二分查找;
2、動態規劃;
3、樹狀數組;

這里用方法2,因為 (1?≤?x[i]?≤?100?000),以x[i]的大小為狀態,數量級在10w,可接受范圍;
先對輸入數據進行處理,對于數字k,我們令a[k]=a[k]+1;(a[i]表示數字i的數量)
我們再用dp[i]表示小于等于i的數量,那么有狀態轉移方程:dp[i] = dp[i-1]+a[i];
對于每個詢問x,我們取一個t=min(x, 1e6),dp[t]就是小于等于x的數量。

C

題目鏈接
題目大意:n個字符串,不能改變字符串的先后順序,可以對每個字符串進行reverse的操作,代價為cost[i],求讓n個字符串按照字符順序排列的最小代價,如果不能輸出-1;
字符串的總長度不會超過10w。

輸入:
第一行 n 表示字符串數量 (2?≤?n?≤?100?000)
第二行 n個數字c[i] 表示對第i個字符串reverse的代價 (0?≤?c[i]?≤?1e9)
接下來 n行 每行是一個字符串;
輸出:
讓n個字符串按照字符順序排列的最小代價,如果不能輸出-1;

Examples
input
2
1 2
ba
ac
output
1

代碼實現


    dp[1][0] = 0;
    dp[1][1] = a[1];
    for (int i = 2; i <= n; ++i) {
        dp[i][0] = dp[i][1] = inf;
        string currentRe = reStr[i];
        string lastRe = reStr[i - 1];
        if (str[i] >= str[i - 1]) {
            dp[i][0] = min(dp[i][0], dp[i - 1][0]);
        }
        if (str[i] >= lastRe) {
            dp[i][0] = min(dp[i][0], dp[i - 1][1]);
        }
        if (currentRe >= str[i - 1]) {
            dp[i][1] = min(dp[i][1], dp[i - 1][0] + a[i]);
        }
        if (currentRe >= lastRe) {
            dp[i][1] = min(dp[i][1], dp[i - 1][1] + a[i]);
        }
    }
    
    sum = min(dp[n][0], dp[n][1]);
    if (sum == inf) {
        sum = -1;
    }
    
    cout << sum << endl;

題目解析
先不考慮代價,從貪心的角度出發,可以得到一個策略:
1、讓最前面的字符串 字典序盡可能?。?br> 2、讓每個字符串僅僅比上一個字符串大一點;
這樣可以優先滿足字典序的要求,但題目還有另外一個要求:總代價盡可能小。
根據上面的思考我們看到,每個字符串只有原串+reverse兩種狀態,也即是字符串只有兩種抉擇。并且在滿足字典序的狀態下,第i個字符串的決策僅取決于第i-1個字符串,與i-2個字符串無關,滿足動態規劃的要求

dp[i][0]表示第i個字符串為原串的最小代價;
dp[i][1]表示第i個字符串為reverse串的最小代價;
把dp數組初始化為inf;(inf是一個很大都是數字)
可能有以下四種狀態轉移。
dp[i][0] = min(dp[i][0], dp[i - 1][0]);
dp[i][0] = min(dp[i][0], dp[i - 1][1]);
dp[i][1] = min(dp[i][1], dp[i - 1][0] + a[i]);
dp[i][1] = min(dp[i][1], dp[i - 1][1] + a[i]);
最終:
sum = min(dp[n][0], dp[n][1]);
如果sum不為inf就存在解。

D

題目鏈接
題目大意
q個操作
操作1 '+':在集合A,加入數x
操作2 '-' :在集合A,刪除數x
操作3 '?':輸入數x,尋找集合A中,與x異或值最大。
x (1?≤?x?≤?10e9)

輸入:
第一行 q,表示q個操作; (1?≤?q?≤?200?000)
接下來q行,每行有字符'+'、 '-' 、 '?' 和 數字 x[i] (1?≤?x[i]?≤?1e9)
輸出:
對于每個'?'操作,輸出集合A中,與x異或值最大。

Example
input
10
+ 8
+ 9
+ 11
+ 6
+ 1
? 3
- 8
? 3
? 8
? 11
output
11
10
14
13

代碼實現

    int n;
    cin >> n;
    
    while (n--) {
        char type[20];
        scanf("%s", type);
        lld x;
        cin >> x;
        sets.insert(0);
        if (type[0] == '+') {
            sets.insert(x);
        }
        else if (type[0] == '-') {
            sets.erase(sets.find(x));
        }
        else {
            lld ans = 0;
            lld sum = 0;
            for (int i = 30; i >= 0; --i) {
                lld k = 1LL << i;
                lld t = sum + k ^ (x & k);
                
                if (sets.lower_bound(t) != sets.end()) { //存在解
                    lld find =  *sets.lower_bound(t);

                    if (find <= t + (k - 1)) {
                        ans += k;
                        sum = t;
                    }
                    else {
                        sum = sum + (x & k);
                    }
                }
                
            }
            cout << ans << endl;
        }
    }

題目解析
簡單的做法,對每個詢問,遍歷查找集合。復雜度太高。
把異或操作按二進制來看,對于每一位,都盡量使其變為1。
那么,可以按照二進制,從高位開始枚舉是否可以為1。
第i位為1,如果x的第i位為1,需要尋找第i位為0數;
如果x的第i位為0,需要尋找第i位為1的數;

如何確定集合里面是否存在第i位為0或者為1的數字?
對于第i位為1,集合A存在大于等于1<<(i-1)的數字,那么就存在第i位為1的數字;
對于第i位為0,集合A存在大于等于0的數字,那么就存在第i位為0的數字;

用multiset和upper_bound來處理,即可。

備注:用字典樹亦可解。

E

題目鏈接
題目大意::N*M的矩陣,共有q個詢問,每次輸入 ai, bi, ci, di, hi, wi, 表示起點為(a,b)和(c,d)的兩個大小為(w,h)的矩陣進行交換;最后輸出變換后矩陣。
(兩個子矩陣不重疊、沒有相交的點) (2?≤?n,?m?≤?1000, 1?≤?q?≤?10?000)

輸入:
第一行 n, m and q (2?≤?n,?m?≤?1000, 1?≤?q?≤?10?000)
接下來是 n*m的數字矩陣 v[i][j] (1?≤?v[i][j]?≤?1e9)
接下來是 q行詢問,每行 ai, bi, ci, di, hi, wi, 表示起點為(a,b)和(c,d)的兩個大小為(w,h)的矩陣進行交換;

輸出:
變換后矩陣

Examples
input
4 4 2
1 1 2 2
1 1 2 2
3 3 4 4
3 3 4 4
1 1 3 3 2 2
3 1 1 3 2 2
output
4 4 3 3
4 4 3 3
2 2 1 1
2 2 1 1

代碼實現


        int x1, y1, x2, y2, h, w;
        scanf("%d%d%d%d%d%d", &x1, &y1, &x2, &y2, &h, &w);
        
        for (int i = 0; i < 4; ++i) {
            stacks1[i].clear();
            stacks2[i].clear();
        }
        Node *p1, *p2;
        // 1
        p1 = &node[x1][0];
        for (int i = 1; i < y1 + w; ++i) {
            p1 = p1->right;
        }
        p2 = &node[x2][0];
        for (int i = 1; i < y2 + w; ++i) {
            p2 = p2->right;
        }
        for (int i = 0; i < h; ++i) {
            //            changeTwoNodeRight(p1, p2);
            stacks1[0].push_back(p1);
            stacks2[0].push_back(p2);
            p1 = p1->bottom;
            p2 = p2->bottom;
        }
        
        // 2
        p1 = &node[0][y1];
        for (int i = 1; i < x1 + h; ++i) {
            p1 = p1->bottom;
        }
        p2 = &node[0][y2];
        for (int i = 1; i < x2 + h; ++i) {
            p2 = p2->bottom;
        }
        for (int i = 0; i < w; ++i) {
            //            changeTwoNodeBottom(p1, p2);
            stacks1[1].push_back(p1);
            stacks2[1].push_back(p2);
            p1 = p1->right;
            p2 = p2->right;
        }
        
        // 3
        p1 = &node[x1][0];
        for (int i = 1; i < y1; ++i) {
            p1 = p1->right;
        }
        p2 = &node[x2][0];
        for (int i = 1; i < y2; ++i) {
            p2 = p2->right;
        }
        for (int i = 0; i < h; ++i) {
            //                changeTwoNodeRight(p1, p2);
            stacks1[2].push_back(p1);
            stacks2[2].push_back(p2);
            p1 = p1->bottom;
            p2 = p2->bottom;
        }
        
        // 4
        p1 = &node[0][y1];
        for (int i = 1; i < x1; ++i) {
            p1 = p1->bottom;
        }
        p2 = &node[0][y2];
        for (int i = 1; i < x2; ++i) {
            p2 = p2->bottom;
        }
        for (int i = 0; i < w; ++i) {
            //                changeTwoNodeBottom(p1, p2);
            stacks1[3].push_back(p1);
            stacks2[3].push_back(p2);
            p1 = p1->right;
            p2 = p2->right;
        }
        
        for (int i = 0; i < h; ++i) {
            changeTwoNodeRight(stacks1[0][i], stacks2[0][i]);
            changeTwoNodeRight(stacks1[2][i], stacks2[2][i]);
        }
        
        for (int i = 0; i < w; ++i) {
            changeTwoNodeBottom(stacks1[1][i], stacks2[1][i]);
            changeTwoNodeBottom(stacks1[3][i], stacks2[3][i]);
        }
        
    }
    

題目解析
因為子矩陣不相交,先看看暴力的做法。
a[N][M]的數組存矩陣,對每個子矩陣的點,交換一遍;復雜度O(NMQ)。
題目中的N*M*Q = 10 ^ 10,不可行。

先看一行的情況
假設有8個數字
1,2,3,4,5,6,7,8
要交換[2,3]和[5,6],正常的做法是把2的值賦值為5,5的值賦值為2,3的值賦值為6,6的值賦值為3;
很容易想到,這個是數組的做法。
如果是鏈表的方式,那么只需把1的下一個指針 和 4的下一個指針交換,3的下一個指針交換和6的下一個指針交換,即可得到交換后的序列。
交換的時間是O(1),查找的時間是O(N)。
對于矩陣,復雜度為O(NQ)= 10^7,可以接受。

具體細節:
每個點,一個bottom指向下面的點,一個right指向右邊的點,那么一個3*3子矩陣需要修改的邊如下:
*TTT*
L000R
L000R
L000R
*BBB*

為了防止修改過程中,再次遍歷節點時導致點位置發生變化。
用stack把需要修改的點存下來。
最后再統一進行修改即可。

TLE之后,查看了別人的做法,發現大同小異。
看起來要進行常數級別的優化,把cin改成scanf,果然就過了。

S###*
#000#
#000#
#000#
*###0

可以優化的地方:遍歷的時候,根據S的位置,繞著矩陣遍歷一遍即可。

總結

前言講了一些最近的感慨,編程能力是由各方面組成的。
算法訓練不是萬能的。在這里,你學不到設計模式,學不到軟件架構,學不到操作系統,學不到網絡原理,學不到數據庫。。。
簡單來說,這個算法訓練讓你智商變高,同時承受能力變強。
因為這個算法訓練是如此的枯燥和無聊,相比之下項目中的需求實現反而是一種簡單而有趣的事情。
舉個例子:最近寫AAC解碼器遇到一個問題,解碼器經常爆出各種奇怪的error信息。為了解決這個問題,我找了很多網上的demo,到蘋果官網對比官方教程。問題足足困擾了我5、6個小時,甚至最后都使出二分代碼的絕招。而這種情況在算法訓練過程中是很正常的。

大多數人在學生時代沒有興趣玩算法,畢業之后更不可能花更多的時間去玩。
畢竟,做好業務需求,時間長了一樣做leader。
反正核心的功能都會有第三方庫,網上還有別人的解決方案。
走架構這一條路也是很多人的選擇。
更何況,寫代碼對有的人來說只是一份工作。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內容