题目列表
一、求出硬币游戏的赢家
简单数学题,115 只能由 一个 75 和 四个 10 组成,我们只要看能构成几115就行,如果有奇数个115则Alice赢,否则Bob赢,代码如下
class Solution {
public:
string losingPlayer(int x, int y) {
// 75 + 10 * 4
return min(x,y/4)%2 ? "Alice":"Bob";
}
};
二、操作后字符串的最短长度
简单的模拟题,说的很复杂,实际上就是看每个字符出现的次数,如果字符出现为奇数,那么能剩下一个,如果为偶数,那么会剩下两个,代码如下
// 直接模拟
class Solution {
public:
int minimumLength(string s) {
int n = s.size();
int cnt[26]{};
for(auto e:s) {
if(++cnt[e-'a']==3)
n-=2, cnt[e-'a']-=2;
}
return n;
}
};
// 可以用数学方式来算
class Solution {
public:
int minimumLength(string s) {
int cnt[26]{};
for(auto e:s) ++cnt[e-'a'];
int ans = 0;
for(auto c:cnt)
if(c > 0)
ans += (c%2?1:2);
return ans;
}
};
三、使差值相等的最少数组改动
这题估计很多人都会想去贪心,觉得 cnt [ abs(nums[i] - nums[n-i-1]) ]多的操作次数就一定少。这里要注意一点,这样做仅仅是让不操作的次数多了,但是没有考虑到有可能会导致操作两次的次数变多,从而导致整体数量变多的情况,所以不能贪心,只能暴力枚举
这里就需要考虑:枚举 X 算答案,还是直接枚举 答案 ,然后检验答案是否合法?这里我们选择枚举 X ,因为它能给我们后面的计算增加条件。
那么如何计算呢?
对于每个数对来说,面对一个X,它只能有3种情况:操作次数为0/1/2
假设,一个数对为 x,y (x <= y)
- 操作0次,此时 X = abs(x - y)
- 操作1次,即可以改变一个数,此时 X <= max(y,k - x) 且 X != abs(x - y)
- 操作2次,此时,X > max(y,k - x) 且 X != abs(x - y)
所以,我们需要统计不同操作次数下的数对数量,用数组记录即可,代码如下
class Solution {
public:
int minChanges(vector<int>& nums, int k) {
int n = nums.size();
// cnt0[i] 记录 X = i 时,操作次数为 0 的数对有几个
// cnt1[i] 记录 操作次数为 1 时,X 最大范围为 i 的数对有几个
vector<int> cnt0(k+1), cnt1(k+1);
for(int i = 0; i < n/2; i++){
int x = nums[i], y = nums[n-i-1];
if(x > y) swap(x, y);
int mx = max(y, k - x);
cnt0[y-x]++;
cnt1[mx]++;
}
int ans = INT_MAX;
int sum2 = 0; // 记录操作次数为 2 的数对有几个
for(int x = 0; x <= k; x++){
// n/2 - sum2 - cnt0[x] :操作次数为 1 的数对个数
ans = min(ans, n/2 - sum2 - cnt0[x] + 2 * sum2);
sum2 += cnt1[x]; // 计算超出操作次数为 1 的最大范围的数有几个,即操作次数为 2 的数对个数
}
return ans;
}
};
上面的代码是基于操作次数来统计数对个数,但其实我们换个角度,算每个数对,针对不同 X 时所需的操作次数(即算贡献),这题就变成了一个差分数组的题。
思路如下:
对于一个数对 x,y (x <= y)
- 当 X 在 [0,max(y,k - x)] 中时,操作次数为 1
- 当 X 在 [ y - x,y - x] 中时,操作次数为 0
- 当 X 在 [max(y,k - x),k] 中时,操作次数为 2
这显然是和区间的+/-操作有关,即应该用差分数组来写,代码如下
class Solution {
public:
int minChanges(vector<int>& nums, int k) {
int n = nums.size();
vector<int> diff(k+2);
for(int i = 0; i < n/2; i++){
int x = nums[i], y = nums[n-i-1];
if(x > y) swap(x, y);
int mx = max(y, k - x);
// 对区间 [0, mx] 进行 +1 操作
diff[0]++; diff[mx+1]--;
// 对区间 [y - x, y - x] 进行 -1 操作
// 因为 y-x 在 [0, mx] 中,之前进行过 +1,现在需要 -1 才能让操作次数为 0
diff[y-x]--; diff[y-x+1]++;
// 对区间 [mx + 1, k] 进行 +1 操作
diff[mx+1] += 2;
// diff[k+1] -= 2 这个可以不用,用不到
}
int ans = INT_MAX;
int pre = 0;
for(int x = 0; x <= k; x++){
pre += diff[x];
ans = min(ans, pre);
}
return ans;
}
};
四、网格图操作后的最大分数
这种题目,我们在思考时,要先将问题拆解为规模更小的问题,也就是从特殊到一般。列数多了我们思考不来,我们就先思考一列的得分如何确定(需要哪些信息),再去考虑更多列的情况。(显然这题是用动规来做)
思路如下:我们要算出第 i 列的得分,就需要知道 第 i - 1,i,i + 1 列 的黑色格子的数量。假设我们从后往前依次对每一列的得分进行计算,在遍历时,我们要知道当前列 i,它的黑格子数量 j,以及它右边的列的黑格子数量 k,对它左边列的黑格子数量,我们进行枚举。这里要注意:枚举的不是当前列的黑格子数量,而是它左边列的黑格子数量,因为我们是往一个方向枚举的,无法先得到它左列的黑格子数量。
所以我们得到如下的递归函数
dfs(i,j,k)表示第 i 列黑格子数量为j,第i+1列黑格子数量为k时,[i,n) 列的最大得分
dfs(i,j,k) = max(dfs(i-1,h,j) + 中间列的得分) h=[0,n]
代码如下
// 记忆化搜索
class Solution {
using LL = long long;
public:
long long maximumScore(vector<vector<int>>& grid) {
int n = grid.size();
LL pre[n+1][n+1]; memset(pre, 0, sizeof(pre));
// 计算每一列的前缀和
for(int j = 0; j < n; j++){
for(int i = 0; i < n; i++){
pre[j][i+1] = pre[j][i] + grid[i][j];
}
}
LL memo[n+1][n+1][n+1];
memset(memo,-1,sizeof(memo));
function<LL(int, int, int)> dfs=[&](int i, int j, int k)->LL{
if(i == 0) return (k > j ? pre[i][k] - pre[i][j] : 0);
if(memo[i][j][k]!=-1) return memo[i][j][k];
LL res = 0;
for(int h = 0; h <= n; h++){
res = max(res, dfs(i-1, h, j) + (max(h, k) > j ? pre[i][max(h, k)] - pre[i][j] : 0));
}
return memo[i][j][k] = res;
};
LL ans = 0;
for(int i = 0; i <= n; i++){
ans = max(ans, dfs(n-1, i, 0));
}
return ans;
}
};
// 递推
class Solution {
using LL = long long;
public:
long long maximumScore(vector<vector<int>>& grid) {
int n = grid.size();
LL pre[n+1][n+1]; memset(pre, 0, sizeof(pre));
for(int j = 0; j < n; j++){
for(int i = 0; i < n; i++){
pre[j][i+1] = pre[j][i] + grid[i][j];
}
}
LL ans = 0;
LL f[n][n+1][n+1];
memset(f,0,sizeof(f));
for(int j = 0; j <= n; j++){
for(int k = j + 1; k <= n; k++){
f[0][j][k] = pre[0][k] - pre[0][j];
}
}
for(int i = 0; i < n - 1; i++){
for(int j = 0; j <= n; j++){
for(int k = 0; k <= n; k++){
LL res = 0;
for(int h = 0; h <= n; h++){
res = max(res, f[i][h][j] + (max(h, k) > j ? pre[i+1][max(h, k)] - pre[i+1][j] : 0));
}
f[i+1][j][k] = res;
}
}
}
for(int i = 0; i <= n; i++)
ans = max(ans, f[n-1][i][0]);
return ans;
}
};
以上是O(n^4)的做法,会超时,但是对我们理解状态定义有帮助,下面在贴一个O(n^3)的做法,就是对上面代码的优化(在逻辑层面上的优化,不是用数据结构辅助完成的优化),这里就不做多解释了,因为优化思路比较复杂,这里仅仅是放个代码。
class Solution {
using LL = long long;
public:
long long maximumScore(vector<vector<int>>& grid) {
int n = grid.size();
LL pre[n+1][n+1]; memset(pre, 0, sizeof(pre));
for(int j = 0; j < n; j++){
for(int i = 0; i < n; i++){
pre[j][i+1] = pre[j][i] + grid[i][j];
}
}
LL memo[n][n+1][2];
memset(memo, -1, sizeof(memo));
// i 表示 第 i 列
// j 表示 第 i+1 列的高度
// k 表示 第 i+2 列的高度是否大于第 i+1 列的高度
function<LL(int, int, bool)> dfs=[&](int i, int j, bool k)->LL{
if(i < 0) return 0;
auto& res = memo[i][j][k];
if(res != -1) return res;
res = 0;
for(int h = 0; h <= n; h++){
if(h == j)
res = max(res, dfs(i-1, h, false));
else if(h < j)
res = max(res, dfs(i-1, h, true) + pre[i][j] - pre[i][h]);
else if(!k)
res = max(res, dfs(i-1, h, false) + pre[i+1][h] - pre[i+1][j]);
else if(j == 0)
res = max(res, dfs(i-1, h, false));
}
return res;
};
LL ans = 0;
for(int i = 0; i <= n; i++){
ans = max(ans, dfs(n-2, i, false));
}
return ans;
}
};