整个过程就是暴力搜索→记忆化搜索→DP。
1. 周赛366 T3 LC 2896 执行操作使字符串相等
这题虽然标的是中等,但只是因为位置在T3而已。实际上灵神对这题的难度分预估是2300,瓜的守门题,碾压一众手速场T4了。
首先是判断能否把s1转成s2。由于每次操作都将反转两个字符,那么两个字符串的海明距离显然要是偶数:
- 反转s1和s2中不同的两个位置,那么海明距离将减少2
- 反转s1和s2中的一个相同的位置,和一个不同的位置,那么海明距离不变(+1-1)
- 反转s1和s2中的两个相同的位置,海明距离+2
也就是所有操作对不同位置的数量的影响一定是加上或减去一个偶数,如果海明距离是奇数,那么将不可能把s1转成s2。
这里判断的具体实现是位异或运算,直接逐位检查,异或。这样最终应该有偶数个1,异或完应该是0;如果奇数个1,异或完是1,直接返回-1即可。
然后是这题的重点环节。
从后往前遍历:
- 对于操作一,我们可以把花费x的代价反转任意两个位置的字符理解为,花费x的代价反转任意一个位置的字符,并且可以免费反转一次前方任一位置的字符。(注意这个免费次数必须消耗掉,不然不等价,后方不用考虑,因为我们从后往前遍历的)
- 对于操作二,就是简单地花费1代价反转前方相邻位置和当前位置的字符。
对于两种选择的分支,直接暴力搜索即可。那么很显然暴搜会T,就空间换时间改成记搜。
令memo[i][j][k]表示,在上一次反转/不反转i索引处元素,且有着j次免费反转次数的情况下,使得s1[0:i]=s2[0:i]要付出的最小代价
。
这样在深搜时就(可能)有三种选择了:
- 不消耗免费的反转次数,使用新的操作一,这样会使得之后的反转次数增1
- 使用操作二,这样下一次递归就要默认当前位置被反转
- 消耗免费的反转次数(这是之前的操作一带来的“福利”),这样会使得之后的反转次数减一
每次在一个状态计算完之后,记录到memo中方便后续剪枝即可。
最终返回memo[n-1][0][0]即可。
细节在代码中:
import java.util.Arrays;
class Solution {
public int minOperations(String s1, String s2, int x) {
// 记搜写法
int n = s1.length();
// 第一个维度:当前字符串索引 [0,n-1]
// 第二个维度:剩余免费次数 [0,n]
// 第三个维度:上一次操作是否反转了i-1
// memo[i][j][k]表示,在上一次反转/不反转i索引处元素,且有着j次免费反转次数的情况下,使得s1[0:i]=s2[0:i]要付出的最小代价
int[][][] memo = new int[n][n+1][2];
// 判断是否能成功把s1转成s2
char[] s = s1.toCharArray();
char[] t = s2.toCharArray();
int diff = 0;
for (int i = 0; i < s.length; i++) {
diff ^= s[i]^t[i]; // s[i]异或t[i],不一样的次数应该是偶数次
}
if(diff!=0){
return -1;
}
for(int i=0;i<n;i++){
for(int j=0;j<=n;j++){
Arrays.fill(memo[i][j],-1); // 初始化备忘录
}
}
return dfs(n-1,0,0,memo,s,t,x);
}
private int dfs(int i, int j, int preRev, int[][][] memo, char[] s, char[] t, int x) {
// 从右往左
if(i<0){
// 全部相等
// 免费次数必须用完,前方不能反转
if(j>0||preRev==1){
return Integer.MAX_VALUE/2; // 代表inf,/2防爆int
}else{
return 0;
}
}
if(memo[i][j][preRev]!=-1){
return memo[i][j][preRev]; // 记忆化剪枝
}
// 当s[i]!=t[i]且preRev=1,恰好这一步不需要反转了,因为已经相等了
// 当s[i]==t[i]且preRev==0,也不需要反转了,相等了
if(
// 顶级trick
(s[i]==t[i]) == (preRev!=1)
){
return dfs(i-1,j,0,memo,s,t,x);
}
// 不用免费次数
//操作一 用做操作一就不能用操作二
int res1 = dfs(i-1,j+1,0,memo,s,t,x)+x;
//操作二 用操作二就不能用操作一
int res2 = dfs(i-1,j,1,memo,s,t,x)+1;
int res = Math.min(res1,res2);
//可以使用免费次数
if(j>0){
int res3 = dfs(i-1,j-1,0,memo,s,t,x);
res = Math.min(res,res3);
}
memo[i][j][preRev] = res;
return res;
}
}
2. LC 3040 相同分数的最大操作数目Ⅱ
由于美赛+过年摆烂,连着3周没打周赛,水平下滑很多。
这题一开始写暴搜剪枝T了,后面想想是个DP,但是一直想不出来怎么从下往上搜,最后写了个记搜,一看提交其实全是记搜,没人写二维数组的DP,被自己绕进去了。
memo(l,r)代表删除到还剩nums[l,r]的子数组时,可以操作的最大次数。每个状态分三种可能的操作分类向下继续搜,遇到记忆的状态直接返回即可。否则搜到底然后记忆好状态返回。
目标和仅有三种情况,nums[0]+nums[n-1] , nums[0]+nums[1] , nums[n-1] + nums[n-2]。分别来次记搜就可以。
class Solution {
int[] targets;
int[][] memo;
public int maxOperations(int[] nums) {
int n = nums.length;
if(n <2){
return 0;
}
targets = new int[]{nums[0]+nums[1],nums[0]+nums[n-1],nums[n-1]+nums[n-2]};
memo = new int[n][n];
int max = 1;
for (int target : targets) {
clearMemo();
max = Math.max(dfs(nums,0,n-1,target),max);
}
return max;
}
private void clearMemo(){
for (int i = 0; i < memo.length; i++) {
Arrays.fill(memo[i],-1);
}
}
private int dfs(int[] nums,int l,int r,int target){
if(r-l+1<2){
return 0;
}
if(memo[l][r]!=-1){
return memo[l][r];
}
int max = 0;
if(nums[l]+nums[l+1]==target){
max = Math.max(max,dfs(nums,l+2,r,target)+1);
}
if(nums[r]+nums[r-1]==target){
max = Math.max(max,dfs(nums,l,r-2,target)+1);
}
if(nums[l]+nums[r]==target){
max = Math.max(max,dfs(nums,l+1,r-1,target)+1);
}
memo[l][r] = max;
return max;
}
}
3. LC 2998 使X和Y相等的最少操作次数
记搜都写不出,给我菜完了。
怎样从x到y呢?
- 若x≤y,那么显然只能是y-x,因为让x增大的操作类型只有增一。
- 若x>y,那么显然其中一个最朴素的答案是一直减一得到y,也即x-y。
- 如果要用除以11,那么最优的只有两种,最接近x的且小于x的11的倍数,和最接近x的且大于x的11的倍数。我之前一直疑惑的是:能否把x增加到一个不是最接近的且是大于x的11的倍数,然后除以11直接得到很接近y的一个数?后来想想不会,一句话就解释掉了:以1为单位增减,一定比以11为单位增减来的快。举个例子,x=50,y=7,难道要增加到77再除以11吗。很显然不如直接减少到44再除以11再加三快啊。后者就是以1为单位的,前者是以11为单位的(跨了2个11)。
- 对于除以5,与3同理。
import java.util.HashMap;
import java.util.Map;
class Solution {
Map<Integer,Integer> memo = new HashMap<>();
public int minimumOperationsToMakeEqual(int x, int y) {
if(x<=y){
return y-x;
}
if(memo.containsKey(x)){
return memo.get(x);
}
int ans = x-y; // 最直接的方式就是逐渐减1得到
ans = Math.min(ans,minimumOperationsToMakeEqual(x/11,y)+x%11+1); // x%11指的是用来删掉x比最接近(<)自己的11的倍数多的部分 1指的是x/11
ans = Math.min(ans,minimumOperationsToMakeEqual(x/11+1,y)+11-x%11+1); //11-x%11指的是把x增加到最接近(>)自己的11的倍数的操作次数 1指的x/11,x/11+1指的是增加后除以11的结果
ans = Math.min(ans,minimumOperationsToMakeEqual(x/5,y)+x%5+1);
ans = Math.min(ans,minimumOperationsToMakeEqual(x/5+1,y)+5-x%5+1);
memo.put(x,ans);
return ans;
}
}
4. LC 3098 求出所有子序列的能量和
VP双周赛127 T4。这题的状态有四个分量:
- 选到哪个
- 还剩多少个待选
- 前一个选的什么
- 最小差值是什么
如果想相邻两个可以更新最小差值,很显然要先排序。
暴搜要T,得记搜,但这么多状态开数组就M了。我看别的java都被逼得拿字符串状压了,真不至于。年少不知python好,错把java当成宝。lc是全python最好的语言,@cache助力memo。
from cmath import inf
from functools import cache
from typing import List
class Solution:
def sumOfPowers(self, nums: List[int], k: int) -> int:
mod = 10 ** 9 + 7
nums.sort()
@cache
def dfs(i, j, pre, min_diff):
if j > i + 1:
return 0
if j == 0:
return min_diff
pick = dfs(i - 1, j - 1, nums[i], min(min_diff, pre - nums[i]))
no_pick = dfs(i - 1, j, pre, min_diff)
return (pick + no_pick) % mod
return dfs(len(nums) - 1, k, inf, inf)
5. LC 741 摘樱桃
这题两次DP的结果和不一定是最优解,因为第二次走回来的子问题就和原问题不同构了。
可以这么考虑:有两个人同时从(0,0)出发,向(n-1,n-1)走。(有可能会疑惑,题目中不是说了,第二个人应该是(n-1,n-1)走到(0,0)吗?但其实它规定了右下角走到左上角只能上左走,左上角走到右下角只能下右走,因此这个思路其实是等价的)
定义dp(t,j,k)为走了t步,且两人分别到了y轴j和k时可以摘到的最多的樱桃数量。(注意既然维护了总步数和y轴距离,那么x轴走出去的距离也能通过总-y轴计算出来了)
那么这两个人上一步的位置可能有四种情况:
- A向下走,B向下走(简称下下)
- 下右
- 右下
- 右右
状态转移方程为:
dp(t,j,k) = max( dp(t-1,j-1,k-1) , dp(t-1,j-1,k) , dp(t-1,j,k-1) , dp(t-1,j,k) ) + curr
其中
$$ curr = \begin{cases} grid[t-j][j] & \text{if } j = k, \\ grid[t-j][j]+grid[t-k][k] & \text{if } j ≠ k \end{cases} $$
import java.util.Arrays;
class Solution {
int[][][] memo;
public int cherryPickup(int[][] grid) {
int n = grid.length;
memo = new int[2*(n-1)+1][n][n];
for (int i = 0; i < memo.length; i++) {
for (int j = 0; j < memo[0].length; j++) {
Arrays.fill(memo[i][j],-1);
}
}
return Math.max(dfs(2*n-2,n-1,n-1,grid),0);
}
private int dfs(int t,int j,int k,int[][] grid){
if(t<j||t<k||j<0||k<0||grid[t-j][j]<0||grid[t-k][k]<0){
return Integer.MIN_VALUE;
}
if(t==0){
return grid[0][0];
}
if(memo[t][j][k]!=-1){
return memo[t][j][k];
}
/*
下下
下右
右下
右右
*/
int max =
Math.max(
Math.max(
dfs(t-1,j-1,k-1,grid),
dfs(t-1,j-1,k,grid)
),
Math.max(
dfs(t-1,j,k-1,grid),
dfs(t-1,j,k,grid)
)
)+(grid[t-j][j]+(j==k?0:grid[t-k][k]));
memo[t][j][k] = max;
return max;
}
}