第47场 LeetCode 双周赛
本次周赛链接
No.1 找到最近的有相同 X 或 Y 坐标的点
给你两个整数 x 和 y ,表示你在一个笛卡尔坐标系下的 (x, y) 处。同时,在同一个坐标系下给你一个数组 points ,其中 points[i] = [ai, bi] 表示在 (ai, bi) 处有一个点。当一个点与你所在的位置有相同的 x 坐标或者相同的 y 坐标时,我们称这个点是 有效的 。
请返回距离你当前位置 曼哈顿距离 最近的 有效 点的下标(下标从 0 开始)。如果有多个最近的有效点,请返回下标 最小 的一个。如果没有有效点,请返回 -1 。
两个点 (x1, y1) 和 (x2, y2) 之间的 曼哈顿距离 为 abs(x1 - x2) + abs(y1 - y2) 。
示例 1:
输入:x = 3, y = 4, points = [[1,2],[3,1],[2,4],[2,3],[4,4]]
输出:2
解释:所有点中,[3,1],[2,4] 和 [4,4] 是有效点。有效点中,[2,4] 和 [4,4] 距离你当前位置的曼哈顿距离最小,都为 1 。[2,4] 的下标最小,所以返回 2 。
解析
本题是例行签到题。题目给定了一个目标点,坐标为 [x, y],同时给定另一组点的坐标,让我们找到这组点中,x或y坐标有一个与目标点相等并且曼哈顿距离最小的点。
本题只需要遍历一遍所有点,首先判断是否存在一个坐标与目标点相同,如果是,那么计算该点到目标点的曼哈顿距离,并更新最小值及下标。
注意初始时将最小值设为最大的整型,下标设为-1即可。这样如果不存在满足题意的点,下标将保持-1不会更新,直接返回也符合题目要求。
C++代码如下:
// 2021.03.06 leetcode bi-weekly contest 47 - code
//No 1
int nearestValidPoint(int x, int y, vector<vector<int>>& points) {
int ans = INT_MAX,index=-1;
for (int i = 0; i < points.size();++i) {
auto p = points[i];
if (x == p[0] || y == p[1]) {
int tmp = abs(p[0] - x) + abs(p[1] - y);
if (tmp < ans) {
ans = tmp;
index = i;
}
}
}
return index;
}
No.2 判断一个数字是否可以表示成三的幂的和
给你一个整数 n ,如果你可以将 n 表示成若干个不同的三的幂之和,请你返回 true ,否则请返回 false 。
对于一个整数 y ,如果存在整数 x 满足 y == 3x ,我们称这个整数 y 是三的幂。
示例 1:
输入:n = 12
输出:true
解释:12 = 3^1 + 3^2
解析
本题让我们判断一个数能否表示为3的幂之和。如果不加限制,显然每个整数都能表示,因为就相当于转化到3进制。不过题目要求是,表示成3的幂之和,每一项的系数最多是1.
换言之,一个数n显然可以写成 n = ∑ i = 0 a i ∗ 3 i n=\sum_{i=0}{a_i*3^i} n=∑i=0ai∗3i,但是题目要求每个 a i a_i ai都不能超过1.三进制表示每一位可以是0,1,2,那么本题就转化为:一个整数的三进制表示中,是否每一位都不是2.
那这样就很容易了,将输入的这个数转化为三进制表示,再去看是不是满足每一位都不是2即可,满足返回true,反之返回false。
C++代码如下:
//No 2
bool checkPowersOfThree(int n) {
vector<int>p;
while (n) {
p.push_back(n % 3);
n /= 3;
}
for (auto k : p) {
if (k == 2) return false;
}
return true;
}
本题还可以进一步简化,将判断放在求三进制表示的循环当中。当然复杂度还是 O ( l o g N ) O(logN) O(logN),但是减小了空间使用,也减小了时间复杂度常数。
No.3 所有子字符串美丽值之和
一个字符串的 美丽值 定义为:出现频率最高字符与出现频率最低字符的出现次数之差。
比方说,“abaacc” 的美丽值为 3 - 1 = 2 。
给你一个字符串 s ,请你返回它所有子字符串的 美丽值 之和。
示例 1:
输入:s = “aabcb”
输出:5
解释:美丽值不为零的字符串包括 [“aab”,“aabc”,“aabcb”,“abcb”,“bcb”] ,每一个字符串的美丽值都为 1 。
解析
本题定义了一个字符串的美丽值,其计算方法是,一个字符串中刚出现次数最多的字符的出现次数,与出现次数最小的次数之差,当然这里出现次数最小,显然至少为1,不考虑未出现的字符。
本题要求给定一个字符串,求它每个子串美丽值的和。
这里先分享一种基本接近于暴力求解的思路,思路很简单,求每个子串出现各字符的次数,找出最多与最少,记录当前子串的美丽值,累加起来。当我们遍历完所有的子串,自然找出了美丽值总和。不过这里显然存在优化空间:
每个字串都可以视作以某个下标开始的字符构成的字符串,以每一个下标作为开始,都存在一组子串,这一组子串的字符出现情况,可以在一次循环中累加得到,而不需要对每个字串从头遍历一次。举例来说,如果我们知道从下标 i i i到 j j j的子串 s [ i ] [ j ] s[i][j] s[i][j]的字符出现情况,那么对于子串 s [ i ] [ j + 1 ] s[i][j+1] s[i][j+1]只要多统计一个字符即可。
字符出现情况可以保存在长度为26的数组中,因为输入仅有小写英文字母构成。
C++代码如下:
//No 3
int beauty(vector<int>&count) {
int minV = INT_MAX, maxV = INT_MIN;
for (auto cc : count) {
if (cc > 0) {
minV = min(minV, cc);
maxV = max(maxV, cc);
}
}
return maxV - minV;
}
int beautySum(string s) {
int ans = 0;
for (int i = 0; i < s.size(); ++i) {
vector<int>count(26, 0);
for (int j = i; j < s.size(); ++j) {
count[s[j] - 'a']++;
ans += beauty(count);
}
}
return ans;
}
首先写一个beauty函数从字符出现数组中遍历一次找到最大值与最小值并计算差值,得到该字符出现状态下的美丽值,这一过程复杂度可以看做 O ( 26 ) O(26) O(26)。对于输入字符串s,外层循环负责遍历所有的子串起始位置 i i i,内层循环逐个遍历起始位置后的每个字符,每遍历一个字符,就相当于考虑了一个子串,将字符加入统计,并计算美丽值,叠加在结果上。总的复杂度是 O ( 26 N 2 ) O(26N^2) O(26N2),按照题意输入字符串长度不超过500,因此符合要求不会超时。
No.4 统计点对的数目
给你一个无向图,无向图由整数 n ,表示图中节点的数目,和 edges 组成,其中 edges[i] = [ui, vi] 表示 ui 和 vi 之间有一条无向边。同时给你一个代表查询的整数数组 queries 。
第 j 个查询的答案是满足如下条件的点对 (a, b) 的数目:
a < b
cnt 是与 a 或者 b 相连的边的数目,且 cnt 严格大于 queries[j] 。
请你返回一个数组 answers ,其中 answers.length == queries.length 且 answers[j] 是第 j 个查询的答案。
请注意,图中可能会有 重复边 。
解析
本题给定一个无向图,包含若干节点与连接它们的边,对于每一对点(a,b),都存在一个cnt值,表示与a或b相连的边的数目,显然这个值就是与a相连的边加上与b相连的边,再减去ab之间的边的数目(计算了两次)。题目又给了一组查询,让我们返回对每个查询值,cnt值大于查询值的点对数目,两个点只构成一个点对,也即只计算a<b的情况。最直接的思路就是根据输入的边的集合,先把每一对点的cnt都算出来,再根据查询数组中每一个的值,找符合要求的点对数目,但由于点的数目最大是 2 ∗ 1 0 4 2*10^4 2∗104,而查询数目最大是20,这样做会超时。我们也没办法对每一个点对的cnt值排序,这样复杂度达到了 O ( N 2 l o g N 2 ) O(N^2logN^2) O(N2logN2)。
这里参考了大佬的思路,其核心在于加速对每个查询值查找符合要求点对的过程。
首先不考虑cnt值中包含(a,b)之间被重复计算的边的情况,我们就认为与a相连的数目加上与b相连的数目比查询值大,就符合题意,那么此时我们可以统计每个点相连的边的数目,并对其排序,对于每个查询值,我们首先枚举一个点a,找出另一个点c使得当相连边数少于c的边数就无法与a构成超过查询值的点对,这样所有比c边数要多的都符合要求,都可以统计在结果中。由于我们事先对每个点边的数目排序,只要知道c的下标,那么c之后的点都符合要求,其数量也直接可得。而且由于我们查找点c都从先定好的点a之后查找,所以尽管排序后我们已经不知道每个点原先的编号,但仍然不会重复计算点对。
这里完成了第一步,也就是不考虑重复计算相连边的点对数目统计,接下来,我们需要对当前统计之中不合题意的去除,所谓不合题意就是虽然a+b大于查询值,但是减掉重复计算的边(ab之间的边)就不大于查询值了。这里可以遍历所有的边,对每条边的顶点p和q,都计算一下是不是p+q>查询值,而p+q-pq之间的边<=查询值,如果是,证明该点对其实不符合要求,应当减掉;反之,如果是p+q小于等于查询值,这种情况在前一步就没统计,也无需去除,要么就是减掉二者之间边数仍然符合大于查询值,应当予以保留。这一步要遍历所有的边,数量级为 1 0 5 10^5 105。
在进行上述步骤之前,首先遍历边的集合,统计每个点的边数,统计每两个点之间的连接数,同时对边去一下重,这样第二步筛选遍历边的时候可以少一点。这里大佬采取了一种编码方式,将两点之间的边的数目存储在哈希表而非二维数组,减小了内存使用与查找时间。
C++代码如下:
//No 4
int encode(int n, int a, int b) {
return max(a, b) * (n + 1) + min(a, b);
}
vector<int> countPairs(int n, vector<vector<int>>& edges, vector<int>& queries) {
vector<int> deg(n + 1, 0);
int ne = edges.size();
unordered_map<int, int> overlap;
vector<vector<int>>disEdges;
for (int i = 0; i < ne; ++i) {
int p = edges[i][0], q = edges[i][1];
deg[p]++;
deg[q]++;
int idx = encode(n, p, q);
if (overlap.find(idx) == overlap.end()) {
disEdges.push_back({ p,q });
}
overlap[idx]++;
}
vector<int> sortedDeg(deg.begin() + 1, deg.end());
sort(sortedDeg.begin(), sortedDeg.end());
int nq = queries.size();
vector<int>ans(nq);
for (int i = 0; i < nq; ++i) {
int l = 0, r = n - 1;
int cnt = 0;
while (l < n) {
while (r > l&& sortedDeg[l] + sortedDeg[r] > queries[i]) {
--r;
}
cnt += (n - max(l, r) - 1);
++l;
}
for (int j = 0; j < disEdges.size(); ++j) {
int p = disEdges[j][0], q = disEdges[j][1];
int idx = encode(n, p, q);
if (deg[p] + deg[q] > queries[i] && deg[p] + deg[q] - overlap[idx] <= queries[i]) --cnt;
}
ans[i] = cnt;
}
return ans;
}