1. 程式人生 > >佇列的一些演算法題

佇列的一些演算法題

leetcode 862

 解題思路:

一、先試試暴力求解。

對於不同的起點

1.如果起點是負數,終止

2.起點依次向後累加,如果累加的值小於等於0,終止

3.如果累加的值大於等於K,符合條件,記錄長度。最後選取最短的長度

複雜度為O(n^{2}),超時。

程式碼如下:

    int shortestSubarray(vector<int>& A, int K) {
        int length = INT_MAX;
        int size = A.size();
        int i, j;
        for(i = 0; i < size; i++) {
            if(A[i] < 0)
                continue;
            int sum = 0;
            for(j = i; j < size; j++) {
                sum += A[j];
                if(sum >= K || sum <= 0 || j-i+1 >= length)
                    break;
            }
            if(sum >= K) {
                int c = j-i+1;
                length = length>c?c:length;
            }
        }
        if(length == INT_MAX)
            length = -1;
        return length;
    }

二、換個思路,嘗試使用佇列

先對陣列A進行一個預處理,使用一個數組p,p[n] = 0 + A[0] + A[1] + ... + A[n-1]  (n >= 0)

那麼p[i] - p[j] 也就是原陣列中的某一段連續的值了(i > j)。

規則1:如果p[i] - p[j] <= 0 ,也就是這一段值的累加為非正數,和暴力求解裡同樣處理——終止。

規則2:如果p[i] - p[j] >= K, 也就是說這一段值的累加滿足條件,停止對p[j]的繼續處理(如果還以j為起點的話,只能得到更長的長度)記錄長度(p[i] - p[j], 不需要再+1), 之後從中選取最短的。

所以實際上用的思想和暴力求解沒有什麼區別。

演算法過程

維護一個雙頭佇列,將p中的值逐個放到隊尾。每加入一個新值,需要進行如下處理:

1. 如果隊尾比要加入的值大,刪除隊尾,直到隊尾比要加入的值小

2. 如果這個值與隊頭的差大於等於K,記錄長度,刪除隊頭

在這個過程中,我們維護的佇列是按升序排列的,所以——為了滿足規則1,我們從後往前找,為了滿足規則2,我們看隊頭。

程式碼如下:

    int shortestSubarray(vector<int>& A, int K) {
        int length = INT_MAX;
        vector<int> p;
        int sum = 0;
        for(auto &i: A) {
            p.push_back(sum);
            sum += i;
        }
        p.push_back(sum);
        int size = p.size();
        deque<int> dq;
        for(int i = 0; i < size; i++) {
            while(!dq.empty() && p[i] < p[dq.back()])
                dq.pop_back();
            while(!dq.empty() && p[i]-p[dq.front()] >= K) {
                length = min(length, i-dq.front());
                dq.pop_front();
            }
            dq.push_back(i);
        }
        return length == INT_MAX?-1:length;
    }

複雜度為O(n)

這個演算法的巧妙之處在於,引入p陣列,在一次遍歷的過程中完成了需要的累加過程,而不是暴力求解中那樣每次重新累加。

leetcode 621

解題思路:

要解此題也就是要找到需要的idle的個數,為了減少idle的使用,應該優先處理出現頻率高的任務,並使用其他任務插入間隔之中,否則最後頻率高的任務會使用到額外的idle作為間隔。

程式碼如下:

struct task {
    char type;
    int num;
};
bool operator<(task a, task b) {
    return a.num < b.num;
}
class Solution {
public:
    int leastInterval(vector<char>& tasks, int n) {
        int count[26] = {0};
        int origin = tasks.size();
        for(auto& i:tasks) {
            count[i-'A']++;
        }
        priority_queue<task> q1;
        priority_queue<task> q2;
        for(int i = 0; i < 26; i++) {
            if(count[i]) {
                q1.push({i+'A', count[i]});
            }
        }
        int idle = 0;
        while(!q1.empty()) {
            int size = n;
            while(!q1.empty() && size+1) {
                task t = q1.top();
                t.num--;
                q1.pop();
                q2.push(t);
                size--;
            }
            while(!q2.empty()) {
                if(q2.top().num != 0)
                    q1.push(q2.top());
                q2.pop();
            }
            if(size+1 > 0 && q1.size() > 0) {
                idle += size+1;
            }
        }
        return origin + idle;
    }
};

leetcode 363

 解題思路:

首先,需要一些前置知識。

1. 最大連續子序列的和的問題

  給定一個數組,如何從中選取出最一個連續的子序列,且它的和最大? 舉個例子,[2, -3, 3, 5] 中的最大子序列為[3,5],和為8。

  演算法:Kadane 演算法

  程式碼如下:

    int maxSubarry(vector<int>& v) {
        int sum = 0, max = 0;
        int size = v.size();
        for(int i = 0; i < size; i++) {
            if(sum < 0)
                sum = v[i];
            else
                sum += v[i];
            max = max>sum?max:sum;
        }
        return max;
    }

如果這一題去掉k這一限制,則可以使用Kadane演算法找到二維矩陣裡的最大字矩陣和。

2. 和小於k的最大連續子序列的問題

對於這一問題,Kadane 演算法不再適用,不能簡單的在原來的基礎上對k進行判斷。例如,[2,2,-1], k = 0, Kadane演算法求得的所有子序列和為 2,4,3 , 沒有滿足的子序列。但實際上子序列-1滿足條件。

演算法:

與上文leetcode 862的解法類似,也需要用到求和陣列,即s[n] = 0 + v[0] + v[1] + ... + v[n-1]  (n >= 0)。在演算法中我們需要使用到set。set會對插入的各個項進行排序,lower_bound函式可以幫助我們選出大於等於指定數值的第一個數。兩個特性結合起來就可以得到大於等於指定數值的最小的數。因為s[i]-s[j]也就是中間的連續序列的和,所以要有s[i]-s[j] <= k  (i > j), 轉換一下, 就是s[j] \geqslant s[i]-k。我們要找s[i]-s[j] 中最大的且小於k的值,當s[i]指定時,就需要找到s[j]中最小的,而lower_bound函式可以幫我們更快地找到。之後再從這些滿足條件的值中篩選出最大值,就是答案了。

另外,需要注意set初始時插入了值0。如果不這麼做,將會影響後面的計算。例如,[2,1], k=3。如果不插入0,最小的sum為2,得到結果就為1,但正確答案應該為3。再例如[2,1], k=2,如果不插入0,第一個sum值2將被跳過(一開始set為空),最後得到的結果為1,但正確答案應該為2。

程式碼如下:

    int maxSubarry(vector<int>& v, int k) {
        int sum = 0, max = INT_MIN;
        set<int> s;
        s.insert(0);
        for(auto& i:v) {
            sum += i;
            auto j = s.lower_bound(sum-k);
            if(j != s.end())
                max = std::max(max, sum-*j);
            s.insert(sum);
        }
        return max;
    }

前置知識講完了,那麼如何將一維的演算法運用到二維矩陣當中呢?

如圖,L棒從左往右移動,每當L棒移動一格,R棒逐格從L棒的位置移動到末尾。在這一過程中,L棒和R棒與矩陣的上下邊界圍成的區域的所有情況就構成了豎著截取出矩陣的所有情況。那麼接下來再在這個基礎上橫切?不,接下來可以直接使用解決和小於k的最大連續子序列的演算法。對於每個矩陣,因為選取了某一列的一行,其他所有列的這一行都會被選中,所以我們可以簡單地橫向將矩形壓縮為陣列,然後執行演算法。最後從所有候選中選出最大的即可。

如果不想看文字表述,也可以聽印度英語(過程很清晰) 。

程式碼如下, 然而並沒有用佇列,與標題不符

class Solution {
public:
    int maxSubarry(vector<int>& v, int k) {
        int sum = 0, max = INT_MIN;
        set<int> s;
        s.insert(0);
        for(auto& i:v) {
            sum += i;
            auto j = s.lower_bound(sum-k);
            if(j != s.end())
                max = std::max(max, sum-*j);
            s.insert(sum);
        }
        return max;
    }
    int maxSumSubmatrix(vector<vector<int>>& matrix, int k) {
        int width = matrix[0].size();
        int height = matrix.size();
        int L = 0, R = 0;
        int max = INT_MIN;
        for(L = 0; L < width; L++) {
            vector<int> column(height, 0);
            for(R = L; R < width; R++) {
                for(int i = 0; i < height; i++)
                    column[i] += matrix[i][R];
                int cur_max = maxSubarry(column, k);
                max = std::max(max, cur_max);
            }
        }
        return max==INT_MIN?-1:max;
    }
};