这周的双周赛没做,但补了题,发现其实不难,挺简单的。
第一题:模拟。
第二题:构造。
第三题:计算几何。
第四题:DP 或者 贪心。
详细题解如下。
1. 统计最大组的数目(Count Largest Group)
2. 构造 K 个回文字符串(Construct K Palindrome Strings)
3.圆和矩形是否有重叠(Circle And Rectangle Overlapping)
LeetCode第23场双周赛地址:
https://leetcode-cn.com/contest/biweekly-contest-23/
1. 统计最大组的数目(Count Largest Group)
题目链接
https://leetcode-cn.com/problems/count-largest-group/
题意
给你一个整数 n 。请你先求出从 1 到 n 的每个整数 10 进制表示下的数位和(每一位上的数字相加),然后把数位和相等的数字放到同一个组中。
请你统计每个组中的数字数目,并返回数字数目并列最多的组有多少个。
示例 1:
输入:n = 13 输出:4 解释:总共有 9 个组,将 1 到 13 按数位求和后这些组分别是: [1,10],[2,11],[3,12],[4,13],[5],[6],[7],[8],[9]。总共有 4 个组拥有的数字并列最多。
提示:
- 1 <= n <= 10^4
解题思路
根据题目意思,我们需要将 1 - n 这 n 个数进行分组,分组标准是,某数字的各位上 求和,和一样的分为同一组。
那么根据数组范围,我们发现,最大数位是 999,也就是说,求和之后,最大和也就是 27 ,那么我们就可以用一个数组记录 cnt[ i ] 记录分到 i 组的数字个数,数组的下标 i 表示,数位求和的结果。
然后我们遍历所有 cnt,得到 其中 cnt[ i ] 数量最多(也就是这个分组中,拥有的数字最多),并统计对应数量最多出现了多少次,也就是答案。
时间复杂度,我们要对每一个数字都求数位和,最多求 4 位,那么就是 O(4 * n)。
AC代码(C++)
class Solution {
public:
int cnt[40];
int cal(int n) // 计算数位之和
{
int val = 0;
while(n)
{
val += (n % 10);
n /= 10;
}
return val;
}
int countLargestGroup(int n) {
memset(cnt, 0, sizeof(cnt));
for(int i = 1;i <= n; ++i)
{
++cnt[cal(i)];
}
int ans = 0;
int mx = 0;
for(int i = 0;i < 40; ++i)
{
if(cnt[i] > mx) // 发现新的最大值,那么更新
{
ans = 1;
mx = cnt[i];
}
else if(cnt[i] == mx) ++ans; // 不然如果 == 最大值,那么出现次数 + 1
}
return ans;
}
};
2. 构造 K 个回文字符串(Construct K Palindrome Strings)
题目链接
https://leetcode-cn.com/problems/construct-k-palindrome-strings/
题意
给你一个字符串 s 和一个整数 k 。请你用 s 字符串中 所有字符 构造 k 个非空 回文串 。
如果你可以用 s 中所有字符构造 k 个回文字符串,那么请你返回 True ,否则返回 False 。
示例 1:
输入:s = "annabelle", k = 2 输出:true 解释:可以用 s 中所有字符构造 2 个回文字符串。 一些可行的构造方案包括:"anna" + "elble","anbna" + "elle","anellena" + "b"
示例 2:
输入:s = "leetcode", k = 3 输出:false 解释:无法用 s 中所有字符构造 3 个回文串。
示例 4:
输入:s = "yzyzyzyzyzyzyzy", k = 2 输出:true 解释:你只需要将所有的 z 放在一个字符串中,所有的 y 放在另一个字符串中。那么两个字符串都是回文串。
提示:
1 <= s.length <= 10^5
s
中所有字符都是小写英文字母。1 <= k <= 10^5
解题思路
根据题意,我们统计每一个字母出现的个数。然后统计 中间 出现 奇数 的字母个数 c。
假设字母总个数为 n (也就是字符串原来的长度)。
我们发现,如果 n < k,那么相当于根据不够分,也就是即使一个字母组成一个字符串,那么也不够 k 个,所以就直接返回 false。
然后我们发现,对于出现 奇数个数 的字母 个数 c,而根据回文串的定义,按理来说,应该要求每一个字母,都是偶数偶数出现的,那么出现奇数个的时候,就拿出一个(剩下就变成偶数了),那么 这些 单独出现的字母,一定是要放在一个回文串的正中间,也就是说,k 个回文串,最多容纳 k 个奇数(放在正中间),所以要求 c <= k,那么当 c > k 的时候,就 false。
上面只是直观的想法,如果要数学证明,就是我们要证明
【证明:】如果 存在答案,那就是要求 c <= k <= n
【必要性】也就是,c <= k <= n 得到,存在答案。首先,当 c <= k 的时候,我们将 c 个 单独出现的,组成 c 个 回文串。那么现在剩下 n - c 个字母,最少要组成 k - c 个回文串。n - c >= k - c,并且,剩下 n - c 个字母 一定都是偶数次的。
我们零 a = k - c, b = n - c。也就是b 个字母,组成 a 个回文串。 a <= b,并且 b 是偶数个字母。
那么这个很简单,如 aabbcc,最多可以组成 6 个回文串,第一个 aabbcc,第二个 aabbc + c,第三个 aabb + c + c ,。。。
所以也就可以组成了。
【充分性】也就是,存在答案,得到 c <= k <= n。上面一开始的分析,就是正面了充分性。
AC代码(C++)
class Solution {
public:
int cnt[26];
bool canConstruct(string s, int k) {
if(s.size() < k) return false;
memset(cnt, 0, sizeof(cnt));
for(char c : s) ++cnt[c - 'a'];
for(int i = 0;i < 26; ++i)
{
if(cnt[i] != 0 && cnt[i] % 2 == 1) --k; // 如果出现奇数,那么 --k (只有当 k >= 0,说明 c <= k)
}
if(k < 0) return false;
return true;
}
};
3.圆和矩形是否有重叠(Circle And Rectangle Overlapping)
题目链接
https://leetcode-cn.com/problems/circle-and-rectangle-overlapping/
题意
给你一个以 (radius, x_center, y_center) 表示的圆和一个与坐标轴平行的矩形 (x1, y1, x2, y2),其中 (x1, y1) 是矩形左下角的坐标,(x2, y2) 是右上角的坐标。
如果圆和矩形有重叠的部分,请你返回 True ,否则返回 False 。
换句话说,请你检测是否 存在 点 (xi, yi) ,它既在圆上也在矩形上(两者都包括点落在边界上的情况)。
示例 1:
示例有图,具体可以看链接 输入:radius = 1, x_center = 0, y_center = 0, x1 = 1, y1 = -1, x2 = 3, y2 = 1 输出:true 解释:圆和矩形有公共点 (1,0)
示例 2:
示例有图,具体可以查看链接 输入:radius = 1, x_center = 0, y_center = 0, x1 = -1, y1 = 0, x2 = 0, y2 = 1 输出:true
示例 3:
示例有图,具体可以查看链接 输入:radius = 1, x_center = 1, y_center = 1, x1 = 1, y1 = -3, x2 = 2, y2 = -1 输出:false
提示:
1 <= radius <= 2000
-10^4 <= x_center, y_center, x1, y1, x2, y2 <= 10^4
x1 < x2
y1 < y2
解题分析
根据,矩形和圆的关系,我们用矩形,将整个空间划为为 9 部分。
第一部分 : 也就是圆心在矩形中,那么就是说明,存在,true
第二部分:圆心在上下左右,那么此时,要有公共点,那就是,要求,圆至少和 四条边,有一条边,相交 或者 相切(涉及到直线 和 圆 的关系)
第三部分:那么只要求,圆和四个顶点,有一个顶点至少在圆内 或者 在 圆上。(那么就是,点 和 圆 的位置关系)
AC代码(C++)
const double EPS = 1e-6;
class Solution {
public:
bool check(int d, int r) // 直线和圆的位置关系
{
if(r >= d) return true;
return false;
}
bool checkD(int r, int x0, int y0, int x1, int y1) // 顶点和圆的位置关系
{
double d = sqrt((x0 - x1) * (x0 - x1) + (y0 - y1) * (y0 - y1));
if(r > d + EPS) return true;
if(fabs(d - r) < EPS) return true;
return false;
}
bool checkOverlap(int r, int x0, int y0, int x1, int y1, int x2, int y2) {
// 第一部分:圆心在矩形中
if(x0 >= x1 && x0 <= x2 && y0 >= y1 && y0 <= y2) return true;
// 第二部分:要至少与其中一条边相切或相交
if(check(abs(y1 - y0), r) && x0 >= x1 && x0 <= x2) return true;
if(check(abs(y2 - y0), r) && x0 >= x1 && x0 <= x2) return true;
if(check(abs(x1 - x0), r) && y0 >= y1 && y0 <= y2) return true;
if(check(abs(x2 - x0), r) && y0 >= y1 && y0 <= y2) return true;
// 圆与四个顶点的位置关系
if(checkD(r, x0, y0, x1, y1) || checkD(r, x0, y0, x1, y2) || checkD(r, x0, y0, x2, y1) || checkD(r, x0, y0, x2, y2)) return true;
return false;
}
};
4.做菜顺序(Reducing Dishes)
题目链接
https://leetcode-cn.com/problems/reducing-dishes/
题意
一个厨师收集了他 n 道菜的满意程度 satisfaction ,这个厨师做出每道菜的时间都是 1 单位时间。
一道菜的 「喜爱时间」系数定义为烹饪这道菜以及之前每道菜所花费的时间乘以这道菜的满意程度,也就是 time[i]*satisfaction[i] 。
请你返回做完所有菜 「喜爱时间」总和的最大值为多少。
你可以按 任意 顺序安排做菜的顺序,你也可以选择放弃做某些菜来获得更大的总和。
示例 1:
输入:satisfaction = [-1,-8,0,5,-9] 输出:14 解释:去掉第二道和最后一道菜,最大的喜爱时间系数和为 (-1*1 + 0*2 + 5*3 = 14) 。每道菜都需要花费 1 单位时间完成。
示例 2:
输入:satisfaction = [4,3,2] 输出:20 解释:按照原来顺序相反的时间做菜 (2*1 + 3*2 + 4*3 = 20)
示例 3:
输入:satisfaction = [-1,-4,-5] 输出:0 解释:大家都不喜欢这些菜,所以不做任何菜可以获得最大的喜爱时间系数。
提示:
n == satisfaction.length
1 <= n <= 500
-10^3 <= satisfaction[i] <= 10^3
解题分析
这道题,其实不难,首先是数据大小,使得不难。然后甚至暴力都可以解决。
根据题意 + 示例分析,我们可以知道,首先先要数组进行排序,也就是,我们要将最大值放到最后做(这样子就可以保证 time 花的时间多,那么乘 起来的 值,会尽可能大)。
同时,注意到 示例 3 中,也都可以不取。但不一定说是 负 的就不取,比如 示例 1,也是取了负值,但是可以抛弃部分大的负值。
方法一、DP
最先想到的就是DP,那么我们设 dp[ i ][ j ],表示,前面 i 道 菜,取了 j 道 的最大值。
那么状态转移方程,我么 对于 dp[ i ][ j ] ,那么我们此时,这道菜,拿 或者 不拿,不拿: dp[ i - 1][ j ]。拿 : dp[i - 1][ j - 1] + 当前满足度 * j。所以 dp[ i ][ j ] = max(dp[ i - 1][ j ], dp[i - 1][ j - 1] + 当前满足度 * j)
这里有几个特殊情况,如果 j == 0,那么 dp[ i ][ 0 ] = 0,也就是,一道菜没拿。
如果 j == i,那就说明,全都拿了,那么 = dp[ i - 1][ j - 1] + 当前满足度 * j。
初始化: 由于是最大值,那么设的是最小值,根据题目意思,最小应该是 0,所以我们设的初始值都是 0
方法二、贪心
分析示例 1 ,当我们排序后之后,-9 -8 -1 0 5,我们发现,是从 -1 开始取,ye就是,我们本来想从 -9 开始取,但是从前到 后,累加 sum = -13 < 0,说明,前面的负数很多,正数不足,那么这样子,当我们从这个位置开始取得时候,就不得。
那么从下一个 -8 开始,此时 sum = sum - (-9) = -4,还是不行
从 -1 开始,sum = sum - (-8) = 4,说明,此时后面正数够了,那么我们得想法,负数会被抵消,那么我们尽可能让 正数 * time 变大,那么就从这个位置开始取了。
所以,我们遍历从某个位置,只要从这个位置开始得 sum >= 0 ,那么就是从这个位置,开始往后取。
其实方法二 可以看成,从最大开始取,那么,我们只要往后取了,相当于 前面得那些数,都多加了一倍,那我们只要,从大数开始取,直到 sum < 0,那么此时不再累加,因为累加,导致 原本得数 + 负数 变小了。
AC代码(方法一、DP C++)
const int MAXN = 510;
class Solution {
public:
int dp[MAXN][MAXN];
int maxSatisfaction(vector<int>& sat) {
int n = sat.size();
sort(sat.begin(), sat.end());
memset(dp, 0, sizeof(dp));
for(int i = 1;i <= n; ++i) // 遍历每道菜
{
for(int j = 1;j <= i; ++j) // 对于当前菜,可能是 才 拿了 j 道菜
{
if(j == i) // j == i,说明,这道菜,一定要拿。
{
dp[i][j] = dp[i - 1][j - 1] + sat[i - 1] * j;
}
else
{
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1] + sat[i - 1] * j);
}
}
}
int ans = 0;
for(int j = 1;j <= n; ++j) ans = max(ans, dp[n][j]);
return ans;
}
};
AC代码(方法二、贪心 C++)
class Solution {
public:
int maxSatisfaction(vector<int>& sat) {
int n = sat.size();
sort(sat.begin(), sat.end());
int sum = 0;
for(int num : sat) sum += num;
int t = 1;
int ans = 0;
for(int i = 0;i < n; ++i)
{
if(sum < 0)
{
sum -= sat[i];
continue;
}
ans += t * sat[i];
++t;
}
return ans;
}
};