LeetCode第223場周賽題解
技術標籤:Leetcode
LeetCode第223場周賽
注:題目來源LeetCode
1720. 解碼異或後的陣列
未知 整數陣列 arr
由 n
個非負整陣列成。
經編碼後變為長度為 n - 1
的另一個整數陣列 encoded
,其中 encoded[i] = arr[i] XOR arr[i + 1]
。例如,arr = [1,0,2,1]
經編碼後得到 encoded = [1,2,3]
。
給你編碼後的陣列 encoded
和原陣列 arr
的第一個元素 first
(arr[0]
)。
請解碼返回原陣列 arr
。可以證明答案存在並且是唯一的。
示例 1: 輸入:encoded = [1,2,3], first = 1 輸出:[1,0,2,1] 解釋:若 arr = [1,0,2,1] ,那麼 first = 1 且 encoded = [1 XOR 0, 0 XOR 2, 2 XOR 1] = [1,2,3] 示例 2: 輸入:encoded = [6,2,7,3], first = 4 輸出:[4,2,0,7,4]
思路:只要利用異或運算的性質,如果有a ^ b = c
,則有b = c ^ a
,a = c ^ b
,時間複雜度
O
(
n
)
O(n)
O(n),空間複雜度
O
(
1
)
O(1)
O(1)
class Solution {
public:
vector<int> decode(vector<int>& encoded, int first) {
vector<int> res;
res.push_back(first);
int pre = first;
for (int i = 0; i < encoded.size(); i++) {
res.push_back(encoded[i] ^ first);
first = res[res.size() - 1];
}
return res;
}
};
1721. 交換連結串列中的節點
給你連結串列的頭節點 head
和一個整數 k
。
交換 連結串列正數第 k
個節點和倒數第 k
個節點的值後,返回連結串列的頭節點(連結串列 從 1 開始索引)。
示例 1: 輸入:head = [1,2,3,4,5], k = 2 輸出:[1,4,3,2,5] 示例 2: 輸入:head = [7,9,6,6,7,8,3,0,9,5], k = 5 輸出:[7,9,6,6,8,7,3,0,9,5] 示例 3: 輸入:head = [1], k = 1 輸出:[1] 示例 4: 輸入:head = [1,2], k = 1 輸出:[2,1] 示例 5: 輸入:head = [1,2,3], k = 2 輸出:[1,2,3]
思路:
方法一:直接遍歷一遍連結串列,並且把對連結串列的遍歷結果儲存到雜湊表中,然後直接交換整數第k個元素和倒數第k個元素的值。時間複雜度 O ( n ) O(n) O(n),空間複雜度 O ( n ) O(n) O(n)
class Solution {
public:
ListNode* swapNodes(ListNode* head, int k) {
vector<ListNode*> vec;
while (head) {
vec.push_back(head);
head = head->next;
}
int n = vec.size();
swap(vec[k - 1]->val, vec[n - k]->val);//交換正數第k個元素和倒數第k個元素
return vec[0];
}
};
方法二:利用快慢指標,首先,使用一個指標firstK
從連結串列頭移動k-1
步指向第k
個元素,然後快指標從firstK
出發,慢指標從頭出發,當快指標到達連結串列末尾時,慢指標指向倒數第k
個元素。然後交換正數第k
個元素和倒數第k
個元素的值。時間複雜度
O
(
n
)
O(n)
O(n),空間複雜度
O
(
1
)
O(1)
O(1)
class Solution {
public:
ListNode* swapNodes(ListNode* head, int k) {
auto firstK = head;
int cnt = k - 1;
while (cnt--) {//定位第k個元素
firstK = firstK->next;
}
auto fast = firstK, slow = head;//快指標從firstK出發,慢指標從頭出發
while (fast->next) {//當快指標到達連結串列末尾時,慢指標指向倒數第k個元素
fast = fast->next;
slow = slow->next;
}
swap(firstK->val, slow->val);//交換正數第k個元素和倒數第k個元素
return head;
}
};
1722. 執行交換操作後的最小漢明距離
給你兩個整數陣列 source
和 target
,長度都是 n
。還有一個數組 allowedSwaps
,其中每個 allowedSwaps[i] = [ai, bi]
表示你可以交換陣列 source
中下標為 ai
和 bi
(下標從 0 開始)的兩個元素。注意,你可以按 任意 順序 多次 交換一對特定下標指向的元素。
相同長度的兩個陣列 source
和 target
間的 漢明距離 是元素不同的下標數量。形式上,其值等於滿足 source[i] != target[i]
(下標從 0 開始)的下標 i
(0 <= i <= n-1
)的數量。
在對陣列 source
執行 任意 數量的交換操作後,返回 source
和 target
間的 最小漢明距離 。
示例 1:
輸入:source = [1,2,3,4], target = [2,1,4,5], allowedSwaps = [[0,1],[2,3]]
輸出:1
解釋:source 可以按下述方式轉換:
- 交換下標 0 和 1 指向的元素:source = [2,1,3,4]
- 交換下標 2 和 3 指向的元素:source = [2,1,4,3]
source 和 target 間的漢明距離是 1 ,二者有 1 處元素不同,在下標 3 。
示例 2:
輸入:source = [1,2,3,4], target = [1,3,2,4], allowedSwaps = []
輸出:2
解釋:不能對 source 執行交換操作。
source 和 target 間的漢明距離是 2 ,二者有 2 處元素不同,在下標 1 和下標 2 。
示例 3:
輸入:source = [5,1,2,4,3], target = [1,5,4,2,3], allowedSwaps = [[0,4],[4,2],[1,3],[1,4]]
輸出:0
思路:這題的關鍵是要得出這樣一個性質,如果allowedSwaps
中有[a, b]
,[b, c]
,那麼source
中通過任意次數交換後,可以得到元素source[a]
,source[b]
, source[c]
的任意排列,也可以從圖論的角度理解這個性質,allowedSwaps
中有[a, b]
,[b, c]
,則在圖中有a
到b
、b
到c
的無向邊,下標a
、b
、c
構成一個聯通分量,通過交換任意次的性質可以得到該聯通分量內任意元素的排列。
這個性質不只是適用於只有2對交換關係。更一般地,可以得出,假設有一個下標集合S
,source
中下標在S
集合中的元素可以交換任意次,那麼可以得到source
中這n
個元素的任意排列,可以使用並查集維護這樣可以任意交換元素的集合。因為我們要計算最小的漢明距離,則我們就將source
中這n
個元素的順序,轉化為與target
中這n
個下標對應的元素的順序相同。設source
中下標集合S
的所有元素構成集合S1
,target
中下標集合S
的所有元素構成集合S2
,如果S1
中有一個元素在S2
中沒有對應元素,那麼漢明距離增加1。
時間複雜度 O ( n l o g n ) O(nlogn) O(nlogn),空間複雜度 O ( n ) O(n) O(n)
class Solution {
public:
vector<int> p;
int find(int x) {
if (p[x] == x) return x;
return p[x] = find(p[x]);
}
int minimumHammingDistance(vector<int>& source, vector<int>& target, vector<vector<int>>& allowedSwaps) {
int n = target.size();
p.resize(n, 0);
for (int i = 0; i < n; i++) p[i] = i;
for (auto &point: allowedSwaps) {
int ra = find(point[0]), rb = find(point[1]);//可以交換source中下標為point[0]和point[1]中的元素
p[ra] = rb;//合併
}
vector<unordered_map<int, int>> group;
vector<unordered_map<int, int>> groupTraget;
unordered_map<int, int> groupIdx;//root --> groupIdx
for (int i = 0; i < p.size(); i++) {//根據並查集中每組的資訊,按照每組將source和target中的元素抽取出來
int root = find(i);
if (groupIdx.count(root)) {
int idx = groupIdx[root];
group[idx][source[i]]++;
groupTraget[idx][target[i]]++;
} else {
groupIdx[root] = group.size();
group.push_back({pair<int, int>(source[i], 1)});
groupTraget.push_back({pair<int, int>(target[i], 1)});
}
}
int res = 0;
for (int i = 0; i < group.size(); i++) {//計算兩個集合中元素的差異,即計算漢明距離
for (auto &[k, v]: group[i]) {
if (groupTraget[i].count(k)) {
if (v > groupTraget[i][k]) res += (v - groupTraget[i][k]);
groupTraget[i].erase(k);
}
else {
res += v;
}
}
}
return res;
}
};
1723. 完成所有工作的最短時間
給你一個整數陣列 jobs
,其中 jobs[i]
是完成第 i
項工作要花費的時間。
請你將這些工作分配給 k
位工人。所有工作都應該分配給工人,且每項工作只能分配給一位工人。工人的 工作時間 是完成分配給他們的所有工作花費時間的總和。請你設計一套最佳的工作分配方案,使工人的 最大工作時間 得以 最小化 。
返回分配方案中儘可能 最小 的 最大工作時間 。
示例 1:
輸入:jobs = [3,2,3], k = 3
輸出:3
解釋:給每位工人分配一項工作,最大工作時間是 3 。
示例 2:
輸入:jobs = [1,2,4,7,8], k = 2
輸出:11
解釋:按下述方式分配工作:
1 號工人:1、2、8(工作時間 = 1 + 2 + 8 = 11)
2 號工人:4、7(工作時間 = 4 + 7 = 11)
最大工作時間是 11 。
提示:
1 <= k <= jobs.length <= 12
1 <= jobs[i]
<=
1
0
7
10^7
107
思路:根據資料範圍可以有效的演算法時間複雜度可能是指數級別,故有效演算法可能是搜尋或者狀態壓縮DP。
方法一:狀態壓縮DP
下面考慮如果使用狀態壓縮DP來該問題。因為最終求解的是工人的工作時間,即完成分配給他們的所有工作花費時間的總和,首先預處理出該值。假設一共有n
個作業(
n
≤
12
n \leq 12
n≤12),那麼一個工人被分配到的所有可能的作業最多有
2
n
2^n
2n種情況(n的所有子集),可以使用一個整數 i
上的低 n
位表示被分配到的作業情況,如果整數i
的第 j
位為1,表示被分配到第jobs[j]
個工作,有了這樣的狀態表示,可以列舉 i
,即 jobs
的所有可能分配方案,對於每個 i
,列舉 i
的每一位,如果 i
的第 j
位為1,去掉第 j
位的任務集合為left
,可以使用遞推式sum[i] = sum[left] + jobs[j]
,計算出當前完成所有當前工作集合所需的工作時間。
下面考慮如何定義動態規劃的狀態以及轉移。動態規劃的狀態可以根據題意定義,題目怎樣設問就怎樣定義,故設f[i][j]
表示前 i
(
0
≤
i
<
k
0\leq i<k
0≤i<k)個人完成工作 j
(
0
≤
j
<
2
n
0 \leq j < 2^n
0≤j<2n)的 最小 的 最大工作時間 ,這裡工作 j
是一個二進位制的狀態表示,j
中為1的為表示當前的工作集合,故最終答案即為f[k - 1][(1 << n)- 1]
。
狀態的轉移需要列舉j
的所有子集s
,考慮計算f[i][j]
,可以將前i - 1
個人完成任務j - s
,第i
個人完成任務s
,記錄每個人的最大工作時間,然後f[i][j]
等於所有方案中去最小的最大工作時間。
時間複雜度
O
(
k
∗
3
n
)
O(k*3^n)
O(k∗3n),空間複雜度
O
(
k
∗
2
n
)
O(k*2^n)
O(k∗2n),n
為jobs
的長度,k
為工人人數。
class Solution {
public:
int minimumTimeRequired(vector<int>& jobs, int k) {
int n = jobs.size();
vector<int> sum(1 << n, 0);//sum[i]表示完成工作集合i所需的工作時間
for (int i = 1; i < (1 << n); i++) {//預處理
for (int j = 0; j < n; j++) {
if (i & (1 << j)) {
int left = i - (1 << j);
sum[i] = sum[left] + jobs[j];
break;//sum[i]不需要重複計算
}
}
}
vector<vector<int>> f(k, vector<int>(1 << n, -1));//f[i][j]前i個人完成工作集合j的最小的最大工作時間
for (int i = 0; i < (1 << n); i++) f[0][i] = sum[i];//一個人完成所有任務所需的時間
for (int i = 1; i < k; i++) {
for (int j = 0; j < (1 << n); j++) {
int minTime = INT_MAX;
for (int s = j; s; s = (s - 1) & j) {
int left = j - s;
int curTime = max(f[i - 1][left], sum[s]);//前i - 1個人完成任務left,第i個人完成任務s
minTime = min(minTime, curTime);
}
f[i][j] = minTime;
}
}
return f[k - 1][(1 << n) - 1];
}
};
方法二:DFS,注意剪枝。
class Solution {
public:
int res = INT_MAX;
int minimumTimeRequired(vector<int>& jobs, int k) {
vector<int> sum(k, 0);
dfs(jobs, k, sum, 0);
return res;
}
void dfs(vector<int> &jobs, int k, vector<int> &sum, int x) {//將jobs的前x個工作分配給k個人,sum[i]表示第i個人的工作時間
if (x == jobs.size()) {
res = min(res, *max_element(sum.begin(), sum.end()));
return;
}
for (int i = 0; i < k; i++) {
if (sum[i] + jobs[x] >= res) continue;//剪枝
sum[i] += jobs[x];
dfs(jobs, k, sum, x + 1);
sum[i] -= jobs[x];
if (sum[i] == 0) break;//剪枝
}
}
};