1. 程式人生 > 實用技巧 >LeetCode周賽#207

LeetCode周賽#207

5519. 重新排列單詞間的空格 #字串 #模擬

題目連結

題意

給定字串text,該字串由若干被空格包圍的單詞組成,也就說兩個單詞之間至少存在一個空格。現要你重新排列空格,使每對相鄰單詞間空格數目都相等,並儘可能最大化該數目。若不能重新平均分配所有空格,請將多餘的空格放置在字串末尾,這也意味著返回的字串應當與原text字串的長度相等。

class Solution {
public:
    string reorderSpaces(string text) {
        int spacenum = 0, words = 0;
        vector<int> pos;
        for (int i = 0; i < text.size(); i++){
            if(text[i] != ' '){
                pos.push_back(i);
                while(i < text.size() && text[i] != ' ') i++;
                words++;
            }
            if(text[i] == ' ') spacenum++;
        }
        if(spacenum == 0) return text;
        int last = (words <= 1) ? spacenum : (spacenum % (words - 1));
        int gap =  (words <= 1) ? 0 : (spacenum / (words - 1));
        string ans;
        for (int i = 0; i < pos.size(); i ++){
            int j = pos[i];
            while(j < text.size() && text[j] != ' ') ans += text[j++];
            if(i != pos.size() - 1) ans += string(gap, ' ');
        }
        ans += string(last, ' ');
        return ans;
    }
};

5520. 拆分字串使唯一子字串的數目最大 #深搜 #雜湊表

題目連結

題意

給定字串s,請拆分該字串,使得拆分出來的若干非空子字串連線後能夠還原為原字串,返回拆分後唯一子字串的最大數目。

分析

起初我採用的是貪心策略qaq,將當前未訪問過的子串加入map,如果訪問過就將該子串連線上後面的字元。然而"addbsd"串得到的答案應是5,也就說拆分成"a","dd","b","s","d",貪心的話會拆分成"a","d","db","s"

觀察資料範圍,串s的長度最多才16,暴搜可行,遞迴起點來進行拆分,用map判斷該子串是否已出現過。

class Solution {
private:
    unordered_map<string, int> vis;
    int ans = -1;
public:
    void DFS(string s, int start, int cnt){
        if(start == s.size())
            ans = max(ans, cnt);
        else if(cnt + (int)s.size() - start + 1 < ans) //注意s.size()一定要強制轉換下
            return; //剪枝,此情況說明,無論如何劃分都無法超過ans值
        else {
            string tmp;
            for (int i = start; i < s.size(); i++){
                tmp += s[i];
                if(vis[tmp] == 0){
                    vis[tmp]++;
                    DFS(s, i + 1, cnt + 1);
                    vis[tmp]--;
                }
            }
        }
    }
    int maxUniqueSplit(string s) {
        DFS(s, 0, 0);
        return ans;
    }
};

5521. 矩陣的最大非負積 #深搜 #動態規劃

題目連結

題意

給定rows*cols的矩陣,矩陣每個元素有一整數(可正可負),你從(0,0)出發,只能向右或向下移動。從(0,0)到達(rows-1, cols-1)的所有路徑中,找出具有最大非負積的路徑,路徑之積為沿路徑訪問的單元格中所有整數的乘積。對最後返回的最大非負積取餘,若該結果為負值,則返回-1

分析

  • DFS版本(\(1856ms\)奇慢)
typedef long long ll;
class Solution {
private:
    ll ans = -1;
    const int MOD = 1e9 + 7;
public:
    void DFS(vector<vector<int>>& grid, int x, int y, ll sum){
        if((x == grid.size() - 1 && y == grid[x].size() - 1) || sum == 0) {
            ans = max(ans, sum); //注意sum==0剪枝,否則超時
            return;
        }
        if(x != grid.size() - 1)
            DFS(grid, x + 1, y, (sum * (ll)grid[x + 1][y]));
        if(y != grid[x].size() - 1)
            DFS(grid, x, y + 1, (sum * (ll)grid[x][y + 1]));
    }
    int maxProductPath(vector<vector<int>>& grid) {
        DFS(grid, 0, 0, (ll)grid[0][0]);
        return ans % MOD; //由於涉及到值間比較,只對最後結果取模即可
    }
};
  • DP版本(\(8ms\))

我們定義一個三維陣列DP[i][j][0\1],代表從(0,0)到達(i,j)的最小乘積和最大乘積。為什麼要維護兩個最值,考慮到乘積的過程中會遇到負數,最大之積乘上負數便成為最小之積了。此外,未到達終點之前移動過程中得到的負數,在後面有機會被另一負數抵消為正數,因而需要維護這兩個最值。注意,遇到負數時,即變號的過程,要切換兩個最值。

typedef long long ll;
class Solution {
private:
    const int MOD = 1e9 + 7;
    ll DP[16][16][2];    
public:
    int maxProductPath(vector<vector<int>>& grid) {
        DP[0][0][1] = DP[0][0][0] = grid[0][0];
        int n = grid.size(), m = grid[0].size();
        for(int i = 1; i < n; i++) //初始化邊界,只有一個方向無需選擇
            DP[i][0][0] = DP[i][0][1] = DP[i - 1][0][1] * (ll)grid[i][0];
        for(int j = 1; j < m; j++) //初始化邊界,只有一個方向無需選擇
            DP[0][j][0] = DP[0][j][1] = DP[0][j - 1][1] * (ll)grid[0][j];
        for(int i = 1; i < n; i++){
            for(int j = 1; j < m; j++){
                if(grid[i][j] == 0) continue;
                else if(grid[i][j] > 0){ //不用變號
                    DP[i][j][1] = max(DP[i][j - 1][1] * (ll)grid[i][j], DP[i - 1][j][1] * (ll)grid[i][j]);
                    DP[i][j][0] = min(DP[i][j - 1][0] * (ll)grid[i][j], DP[i - 1][j][0] * (ll)grid[i][j]);
                }
                else if(grid[i][j] < 0){ //乘上grid[i][j]會使結果變號
                    DP[i][j][1] = max(DP[i][j - 1][0] * (ll)grid[i][j], DP[i - 1][j][0] * (ll)grid[i][j]); //最大值成負值變為最小值
                    DP[i][j][0] = min(DP[i][j - 1][1] * (ll)grid[i][j], DP[i - 1][j][1] * (ll)grid[i][j]);
                }
            }
        }
        if(DP[n - 1][m - 1][1] < 0) return -1;
        else return DP[n - 1][m - 1][1] % MOD;
    }
};

1595. 連通兩組點的最小成本 #狀壓DP #列舉子集 #滾動陣列

題目連結

題意

給定兩組點,第一組有size1個點,第二組有size2個點,size1>=size2。給定cost[][]陣列,cost[i][j]代表第一組點i和第二組點j的連線成本。現要你將第一組的每個點都必須至少與第二組中一個點連線,且第二組的每個點都必須至少與第一組中的一個點連線,返回連通兩組點所需的最小成本。

分析

這道題,讓我意識到我狀壓DP練習得還不夠qaq。

由於第一組點的數量大於第二組點,也就說狀態比第二組點多,那麼定義dp[i][state]代表當前遍歷到左側的第i個節點時,與第二組頂點的連線狀態state(二進位制意義,連線為1,未連線為0)下,所需的最小成本

對於左側第i個節點,它有兩種連線方案:

  1. 選擇右側中已被連線的一個點,進行連線(保證所有的左側點均與右側點相連)。也許你會覺得多餘,但是你看上圖中的頂點3,它只能與A匹配(之前被頂點1連線過)。

  2. 選擇右側中未被連線的若干個點,進行連線。(保證所有右側點均被左側點相連)如上圖中的頂點2。此處要連線的數量,一時無法確定下來,故需要列舉,也就說列舉當前未連線點狀態的子集,我們不妨將這狀態反轉(未連線的表示1,與上面不同,這樣是便於與上一狀態進行合併,按位或操作取並集),比如當前狀態00111說明,右側3,4,5點未被左側點連線,那麼該狀態子集為00001,00010,00011,….。

    [位運算列舉子集的方法]:參考自@lucifer1004的筆記

    模板即為

    for(int state = 0; state < mymax; state++){ //列舉所有狀態
        int rest = ....
        for(int subState = res; subState; subState = (subState - 1) & res){
            //....
        } //從後往前,列舉子集:即不斷-1的過程中,對狀態-1得到的結果與最初母集進行按位和操作,比如"1100",1100-1=1011,我們看到後面兩位的11並不是"1100"的子集,那麼將1011按位與1100得到1000,屬於1100的子集
    }
    

    n個元素所有子集進行列舉,時間複雜度\(O(3^n)\)

狀態壓縮DP往往能通過滾動陣列節省空間,注意到每次迭代左側的一個頂點時,當前新狀態只依賴於上一狀態結果即可(即當前頂點的上一個頂點或上一輪迴圈計算的結果),因而我們可以對dp[i][state]的第一維滾掉,只需定義dp[state]即可。

class Solution {
private:
    int dp[5000];
public:
    int connectTwoGroups(vector<vector<int>>& cost) {
        int n = cost.size(), m = cost[0].size(), mymax = 1 << m;
        for (int i = 0; i <= mymax; i++) dp[i] = 0x3f3f3f3f;
        dp[0] = 0;
        for (int i = 0; i < n; i++) { //迭代每一行(即左側的每一個頂點)
            int tmp[5000]; for (int i = 0; i <= mymax; i++) tmp[i] = 0x3f3f3f3f;
            for (int state = 0; state < mymax; state++) { //列舉 第二組連線的所有狀態
                if (dp[state] == 0x3f3f3f3f) continue; //狀態不可達
                /*方案一:對於左側點,選擇右側已被連線的一個點匹配*/
                for (int j = 0; j < m; j++) {
                    int nextState = state | (1 << j);
                    tmp[nextState] = min(tmp[nextState], dp[state] + cost[i][j]);
                }
                /*方案二:對於右側點,選擇state狀態所有未被連線的點的一個子集裡的點進行連線。*/
                int rest = (mymax - 1) ^ state; //表示截至第 i 行還沒被選過的邊(即未連上第二組相應頂點)
                for (int subState = rest; subState; subState = (subState - 1) & rest) { //列舉之前未連線的邊的子集
                    int sum = 0;
                    for (int j = 0; j < m; j++) //計算該子集中未連線的邊所需要的花費
                        if (subState & (1 << j)) sum += cost[i][j]; //若子集中存在該邊,計入
                    int nextState = state | subState; //之前的邊 並上 部分之前未連線的邊
                    tmp[nextState] = min(tmp[nextState], dp[state] + sum); //更新新狀態
                }
            }
            for (int i = 0; i <= mymax; i++) dp[i] = tmp[i]; //滾掉上一輪的狀態,更新當前輪的狀態,節省空間
        }
        return dp[mymax - 1]; //返回第二組所有頂點均被連上之狀態 的 值
    }
};