Leetcode 周赛第418场
为什么记录周赛内容,主要本人比较懒惰,想通过写博客的方式激励自己,这是参加的第一场周赛,希望下面每场周赛都加油都参加。
题目一
原题链接,题目的大概意思是要求出拼接的三个数,使其二进制的表示最大。
本题的核心思想是贪心, 将数字按照如下的排序顺序从小到大排序(下面的代码解释使用c++, 完整的过程有c++,python两个不同的版本)
public static bool cmp(int a, int b) {
int pa = (a << (__lg(b) + 1) | b;
int pb = (b << (__lg(a) + 1) | a;
return pa > pb;
}
对于贪心的证明:
我们可以采用交换法,如果我们将上述贪心思路得到的顺序序列中的相邻下标为 i , j ∈ 0 , 1 , 2 i, j\in{0, 1, 2} i,j∈0,1,2进行交换,那么此时在相同长度下得到的是 p j pj pj 但是我们贪心思路下得到的是 p i pi pi,因此贪心成立,即贪心得到的是最优解。
// c++的解法
class Solution {
public:
static bool cmp(int a, int b) {
int pa = (a << (__lg(b) + 1)) | b;
int pb = (b << (__lg(a) + 1)) | a;
return pa > pb;
}
int maxGoodNumber(vector<int>& nums) {
sort(nums.begin(), nums.end(), cmp);
cout << (1 << (__lg(2) + 1) | 2) << " " << (2 << (__lg(1) + 1) | 1) << endl;
int ans = 0;
for (auto& num: nums) {
cout << num << endl;
ans = ans << (__lg(num) + 1) | num;
}
return ans;
}
};
// python 的解法
class Solution:
def maxGoodNumber(self, nums: List[int]) -> int:
def cmp(a, b):
pa = (a << b.bit_length()) | b
pb = (b << a.bit_length()) | a
return pb - pa
nums.sort(key=cmp_to_key(cmp))
print(nums)
ans = 0
for num in nums:
ans = ans << num.bit_length() | num
return an
注意Python中的比较函数返回值是int, 如果返回的正值,那么a排在b的后面,如果返回的负值,a排在b的前面。
总结:在python中我们使用 s . b i t _ l e n g t h ( ) s.bit\_length() s.bit_length()快速得到一个数字的二进制表示长度,在c++中我们使用 _ _ l g ( s ) + 1 \_\_lg(s) + 1 __lg(s)+1来快速得到一个数字二进制表示长度
题目二
这道题目核心性质就是使用dfs/bfs/并查集,将所有的可疑方法全部标记出来,然后进行是否移除的判断,也就是是否存在非可疑点调用了可疑点(核心)
// c++
// 存边
class Solution {
public:
vector<int> remainingMethods(int n, int k, vector<vector<int>>& invocations) {
vector<vector<int>> edges(n); // 存边
// 将可疑函数全部记载下来
vector<bool> isDoubt(n, false);
for (auto &v: invocations) {
// caller 调用callee
edges[v[0]].emplace_back(v[1]);
}
isDoubt[k] = true;
// 使用dfs遍历可疑点调用的所有可疑方法
auto dfs = [&] (auto&& dfs, int root) -> void {
for (auto &t: edges[root]) {
if (!isDoubt[t]) {
isDoubt[t] = true;
dfs(dfs, t);
}
}
};
dfs(dfs, k); // 标记所有的可疑点
// 如果存在非可疑方法调用了可疑方法
for (auto& e: invocations) {
if (!isDoubt[e[0]] && isDoubt[e[1]]) {
vector<int> ans(n);
iota(ans.begin(), ans.end(), 0);
return ans;
}
}
// 移除所有可疑方法
vector<int> ans;
for (int i = 0; i < n; i++) {
if (!isDoubt[i]) {
ans.emplace_back(i);
}
}
return ans;
}
};
上述写法可以学到两种东西,第一个是lambda表达式如何写递归,这有点像cs61A中的一个困难的练习,还有一个就是如何快速以递增的方式填充一个容器std::iota(vec.begin(), vec.end(), 0);
# python
class Solution:
def remainingMethods(self, n: int, k: int, invocations: List[List[int]]) -> List[int]:
edges = [[] for _ in range(n)] # 存边
for x, y in invocations:
edges[x].append(y)
# 收集所有的可以方法
DoubtSet = set()
def dfs(root):
DoubtSet.add(root)
for t in edges[root]:
if t not in DoubtSet:
dfs(t)
dfs(k) # 找出所有的可疑点
# 如果非可疑点调用了可以可疑点
for x, y in invocations:
if x not in DoubtSet and y in DoubtSet:
return list(range(n))
return list(set(range(n)) - DoubtSet)
题目三
原题链接
这道题目的核心性质就是我们要能够快速找出第一行应该放置什么样的元素,以及我们应该针对第一行可以放置怎么样的元素做考虑,下面说的点数是指相邻的点的个数。
如果有元素的点数为1,那么证明最多只能放置一行,如果没有元素的点数为4,那么证明最多放置2行,其余情况可以放置多行,至于为什么 要考虑放置2行的情况,是因为我们考虑放置第一行时(如果不是最多可以放置一行的时候),我们采取2-3-3-…-3-2的形式放置数字,那么2行会导致我们枚举的第一行变成蛇形,无法构成一行,因此在两行的时候我们需要第一行放置成2-2型,因此我们可以知道第一行的唯一表示形式就是
对于有点数为1的情况 ( 1 , 2 , 2 ⋯ , 2 ) (1, 2, 2\cdots, 2) (1,2,2⋯,2)
对于没有点数为4的情况 ( 2 , 2 ) (2,2) (2,2)
其他情况 ( 2 , 3 , ⋯ , 3 , 2 ) (2, 3, \cdots, 3, 2) (2,3,⋯,3,2)
// c++实现
class Solution {
public:
vector<vector<int>> constructGridLayout(int n, vector<vector<int>>& edges) {
// 建立图形
vector<vector<int>> g(n);
for (auto& v: edges) {
g[v[0]].push_back(v[1]);
g[v[1]].push_back(v[0]);
}
// 每个度数对应一个点, 最多有四个点
vector<int> degree_to_node(5, -1);
for (int i = 0; i < n; i++) {
degree_to_node[g[i].size()] = i; // 如果0有三个邻居的话,度数为三就对应0
}
// 分情况讨论
vector<int> row;
if (degree_to_node[1] != - 1) {
// 存在度数为1的点
row.emplace_back(degree_to_node[1]);
} else if (degree_to_node[4] == -1) {
// 只有两列
row.emplace_back(degree_to_node[2]);
// 寻找下一个度数为2的点
for (int t: g[degree_to_node[2]]) {
if (g[t].size() == 2) {
row.emplace_back(t);
break;
}
}
} else {
// 证明存在多列
// 先将其中一个2的端点加入
int x = degree_to_node[2];
row.emplace_back(x);
int pre = x;
x = g[x][0];
while (g[x].size() > 2) {
row.emplace_back(x);
for (int t: g[x]) {
if (t != pre && g[t].size() < 4) {
pre = x;
x = t;
break;
}
}
}
// 将最后一个2的端点加入
row.emplace_back(x);
}
// 利用第一列构造整个答案
vector<vector<int>> ans(n / row.size());
ans[0] = move(row);
vector<bool> isVisited(n, false);
for (int i = 0; i < ans[0].size(); i++) isVisited[ans[0][i]] = true;
for (int i = 1; i < ans.size(); i++) {
for (int x: ans[i - 1]) {
cout << x << endl;
for (int t: g[x]) {
if (!isVisited[t]) {
isVisited[t] = true;
ans[i].push_back(t);
break;
}
}
}
}
return ans;
}
};
# python 实现
class Solution:
def constructGridLayout(self, n: int, edges: List[List[int]]) -> List[List[int]]:
# 建图
g = [[] for _ in range(n)]
for x, y in edges:
g[x].append(y)
g[y].append(x)
# 将每个度数与一个点对应
degree_to_node = [-1] * 5
for i, e in enumerate(g):
degree_to_node[len(e)] = i # 例如如果0的度数为3,那么degree_to_node[3] = 0
# 分情况讨论 找出第一列应该放什么
row = []
if degree_to_node[1] != -1:
row.append(degree_to_node[1])
elif degree_to_node[4] == -1:
row.append(degree_to_node[2]) # 先放第一个度数为2的点
for i in g[degree_to_node[2]]:
if len(g[i]) == 2:
row.append(i)
break # 将第二个度数为2的点放入
else:
# 2-3-3-3-2
x = degree_to_node[2]
row.append(x) # 放入第一个度数为2的点
pre = x
x = g[x][0] # 此时x的度数为3
while len(g[x]) > 2:
row.append(x)
for t in g[x]:
if pre != t and len(g[t]) < 4:
# row.append(t)
pre = x
x = t
break
row.append(x)
print(row)
isVisited = [False for i in range(n)]
ans = [[] for i in range(n // len(row))]
ans[0] = row
for num in ans[0]:
isVisited[num] = True
# 循环枚举下一行应该防什么
for i in range(1, len(ans)):
for x in ans[i - 1]:
for y in g[x]:
if not isVisited[y]:
ans[i].append(y)
isVisited[y] = True
break
return ans
注意这里的细节就是枚举完一个点后就要break,不然会出现很多重复的点,导致每一行都加了很多点,最后爆空间(T_T)
题目四
本题的核心是要快速统计出每一个gcd = i的数对个数,注意到如下的数学性质
数学性质:记 c = 所有 i 的倍数 − g c d [ 2 ∗ i ] − g c d [ 3 ∗ i ] − ⋯ − g c d [ n ∗ i ] c=所有i的倍数-gcd[2*i] - gcd[3*i] -\cdots - gcd[n *i] c=所有i的倍数−gcd[2∗i]−gcd[3∗i]−⋯−gcd[n∗i],其中n*i < 数组中最大的数
那么gcd[i] = c ∗ ( c − 1 ) 2 \frac{c*(c -1)}{2} 2c∗(c−1)
这样我们就可以在 O ( N l g ( N ) ) O(Nlg(N)) O(Nlg(N))的时间复杂度内求出gcd[i]的值
//c++
class Solution {
public:
vector<int> gcdValues(vector<int>& nums, vector<long long>& queries) {
int n = *max_element(nums.begin(), nums.end());
vector<int> cnt(n + 1, 0);
for (auto& num: nums) cnt[num] += 1;
vector<long long> cnt_gcd(n + 1, 0);
for (int i = n; i > 0; i--) {
// 枚举每一个cnt_gcd[i] 的个数
// cnt_gcd[i] = 所有i的倍数 - cnt_gcd[2i] - cnt_gcd[3i] - ...
int c = 0;
for (int j = i; j < n + 1; j += i) {
c += cnt[j];
cnt_gcd[i] -= cnt_gcd[j];
}
cnt_gcd[i] += (long long)c * (c - 1) / 2;
}
for (auto num:cnt_gcd) cout << num << endl;
// for (auto num:cnt_gcd) cout << num << endl;
// return vector<int>(n, 0);
vector<long long> s(n + 1, 0);
s[0] = cnt_gcd[0];
for (int i = 1; i < n + 1; i++) s[i] = s[i - 1] + cnt_gcd[i];
// for (auto& num: s) cout << num << endl;
vector<int> ans;
for(int i = 0; i < queries.size(); i ++) {
ans.push_back(upper_bound(s.begin(), s.end(), queries[i]) - s.begin());
}
return ans;
}
};
#python
class Solution:
def gcdValues(self, nums: List[int], queries: List[int]) -> List[int]:
mx = max(nums) # 找出最大的数是多少
cnt = [0] * (mx + 1)
for x in nums:
cnt[x] += 1
cnt_gcd = [0] * (mx + 1)
for i in range(mx, 0, -1):
c = 0
for j in range(i, mx + 1, i):
c += cnt[j]
cnt_gcd[i] -= cnt_gcd[j]
cnt_gcd[i] += (c - 1) * c // 2
print(i, cnt_gcd[i])
s = list(accumulate(cnt_gcd))
print(s)
return [bisect_right(s, q) for q in queries]
注意python中bisect_left返回的是有序序列中第一个大于等于元素的位置,bisect_right返回的是有序序列中第一个大于该元素的位置,c++中upper_bound返回的是有序序列中第一个大于元素的位置,lower_bound返回的是有序序列中第一个大于等于元素的位置。