前兩天 Day 99 的時候,做過一道順子的題目,當時有一個網友的妙解有點沒看懂,今天我來給大家詳細講解一下。
題目鏈接
LeetCode 846. 一手順子[1]
往期回顧:
【每日算法Day 99】你們可能不知道只用20萬贏到578萬是什麼概念[2]
題目描述
盧本偉有一手(hand)由整數數組給定的牌。
現在她想把牌重新排列成組,使得每個組的大小都是 W
,且由 W
張連續的牌組成。
如果她可以完成分組就返回 true
,否則返回 false
。
說明:
-
1 <= hand.length <= 10000
-
0 <= hand[i] <= 10^9
-
1 <= W <= hand.length
示例1
輸入:
hand = [1,2,3,6,2,3,4,7,8], W = 3
輸出:
true
解釋:
盧本偉的手牌可以被重新排列爲 [1,2,3],[2,3,4],[6,7,8]。
示例2
輸入:
hand = [1,2,3,4,5], W = 4
輸出:
false
解釋:
盧本偉的手牌無法被重新排列成幾個大小爲 4 的組。
題解
這題的妙解來自於題解區網友 zhanzq
,當時沒怎麼看懂,現在我來給大家講解一下。
網友題解地址[3]
我們用一個例子來講解:
假設 W = 3
,給定的手牌正好是三個順子:[1,2,3], [2,3,4], [6,7,8]
。
那麼我們統計出每張牌的數量,並且從小到大排序,記爲 count
,這裏就是 [1,2,2,1,0,1,1,1,0]
,並且在數字不連續處和末尾補 0
(作用後面會詳細說)。
- 然後從小到大遍歷每一張牌,首先
1
只有一張,那麼如果它和後面牌能構成順子,那麼2, 3
至少要有一張才行,於是total
數組後面兩個位置都加上1
。 - 然後遍歷到
2
,因爲2
的數量是大於該位置處的total
值的,所以2
的數量足夠滿足前面的牌順子要求。此外2
還會多出一張,那麼後面兩個位置至少要有一張牌才行,於是total
後面兩個位置再加上1
。 - 然後遍歷
3, 4
,發現數量正好都等於total
,那說明它倆正好和前面的牌構成順子,一點都不會多餘。 - 然後遍歷到
0
了,這就說明和前面的牌斷開了。如果這時候total
不爲0
,就說明中間缺失了一些牌,前面存在順子沒法補足結尾。而如果最開始沒有填充0
的話,就沒有辦法判斷這裏的牌是否和前面連續的,你就有可能把6
這張牌直接接到4
後面組成順子了。 - 然後遍歷
6, 7, 8
同理,在對應位置處更新total
就行了。 - 最後遍歷
0
,發現total
也是0
,那就說明整副牌可以構成順子,完美!
時間複雜度是 ,這題數據不強也可以過的。
有沒有辦法優化呢?其實更新 total
這一步可以優化掉 這個複雜度,直接 更新 total
。
- 首先遍歷
1
,因爲1
只有一張,那麼如果它和後面牌能構成順子,那麼2, 3
至少要有一張才行。但是這裏我們不對這幾張牌的total
加上一,而是在這個順子結尾的下一張牌處的deltas
減去1
。 - 然後遍歷
2
,那麼這時候沒有total
了,怎麼計算應該扣除多少前面順子需要的2
呢?其實只需要用前一張牌的牌數加上當前的deltas
值就行了。爲什麼呢?前面一張牌有多少張,你當前這張就得至少有那麼多去構成順子,但是如果前面一張牌是某些順子的結尾,你還得扣掉一些,而扣掉的數值正好就是當前的deltas
,這在前面順子的開頭處已經記錄過了。 - 後面操作類似,就不詳細闡述了。
這種方法精髓就在於,不需要直接更新所有的 total
值,只需要在順子結尾下一個元素處更新一下 deltas
就行了,每次的 total
可以通過上一張牌的 count
和當前的 deltas
推算出來。
這樣總的時間複雜度就降到了 ,近似 。
不得不說,這個方法還是非常妙的,反正我是一下子想不到的,看了代碼都想了很久纔想通。
代碼
暴力更新(c++)
class Solution {
public:
bool valid(vector<int> &count, int W) {
int n = count.size();
vector<int> total(n, 0);
for (int i = 0; i < n; ++i) {
if (count[i] > total[i]) {
int delta = count[i] - total[i];
for (int j = i; j < i+W && j < n; ++j) total[j] += delta;
} else if (count[i] < total[i]) {
return false;
}
}
return true;
}
bool isNStraightHand(vector<int>& hand, int W) {
int n = hand.size();
if (W == 1) return true;
if (n%W) return false;
sort(hand.begin(), hand.end());
vector<int> count;
int i = 0, j = 0;
while (i < n) {
while (j < n && hand[i] == hand[j]) j++;
count.push_back(j-i);
if (j >= n) break;
else if (hand[j] != hand[j-1]+1) count.push_back(0);
i = j;
}
count.push_back(0);
return valid(count, W);
}
};
優化(c++)
class Solution {
public:
bool valid(vector<int> &count, int W) {
int n = count.size(), pre = 0;
vector<int> deltas(n, 0);
for (int i = 0; i < n; ++i) {
pre += deltas[i];
if (pre < count[i]) {
int delta = count[i] - pre;
pre = count[i];
if (i + W < n) deltas[i+W] -= delta;
} else if (pre > count[i]) {
return false;
}
}
return true;
}
bool isNStraightHand(vector<int>& hand, int W) {
int n = hand.size();
if (W == 1) return true;
if (n%W) return false;
sort(hand.begin(), hand.end());
vector<int> count;
int i = 0, j = 0;
while (i < n) {
while (j < n && hand[i] == hand[j]) j++;
count.push_back(j-i);
if (j >= n) break;
else if (hand[j] != hand[j-1]+1) count.push_back(0);
i = j;
}
count.push_back(0);
return valid(count, W);
}
};
參考資料
[1]
LeetCode 846. 一手順子: https://leetcode-cn.com/problems/hand-of-straights/
[2]
【每日算法Day 99】你們可能不知道只用20萬贏到578萬是什麼概念: https://godweiyang.com/2020/04/13/leetcode-846/
[3]
網友題解地址: https://leetcode-cn.com/problems/hand-of-straights/solution/onlognsuan-fa-by-zhanzq/