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
個節點,它有兩種連線方案:
-
選擇右側中已被連線的一個點,進行連線(保證所有的左側點均與右側點相連)。也許你會覺得多餘,但是你看上圖中的頂點
3
,它只能與A
匹配(之前被頂點1
連線過)。 -
選擇右側中未被連線的若干個點,進行連線。(保證所有右側點均被左側點相連)如上圖中的頂點
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]; //返回第二組所有頂點均被連上之狀態 的 值
}
};