动态规划猜法中外部信息简化的相关问题(上)

news2024/11/25 20:18:30

文章目录

  • 1、Leetcode 312.戳气球(困难)
    • 1.1 题目描述
    • 1.2 思路分析
    • 1.3 代码实现
    • 1.4 启示
  • 2、Leetcode 546.移除盒子(困难)
    • 2.1 题目描述
    • 2.2 思路分析
    • 2.3 代码实现
  • 3、消除字符
    • 3.1 题目描述
    • 3.2 思路分析
    • 3.3 代码实现

1、Leetcode 312.戳气球(困难)

1.1 题目描述

给定一个数组 arr,代表一排有分数的气球。

每打爆一个气球都能获得分数,假设打爆气球的分数为 X X X,获得分数的规则如下:

  1. 如果被打爆气球的左边有没被打爆的气球,找到离被打爆气球最近的气球,假设分数为 L L L;如果被打爆气球的右边有没被打爆的气球,找到离被打爆气球最近的气球,假设分数为 R。 获得分数为 L ∗ X ∗ R L*X*R LXR
  2. 如果被打爆气球的左边有没被打爆的气球,找到离被打爆气球最近的气球,假设分数为 L L L;如果被打爆气球的右边所有气球都已经被打爆。获得分数为 L ∗ X L*X LX
  3. 如果被打爆气球的左边所有的气球都已经被打爆;如果被打爆气球的右边有没被打爆的气球,找到离被打爆气球最近的气球,假设分数为 R R R。获得分数为 X ∗ R X*R XR
  4. 如果被打爆气球的左边和右边所有的气球都已经被打爆。获得分数为 X X X

目标是打爆所有气球,获得每次打爆的分数。通过选择打爆气球的顺序,可以得到不同的总分,请返回能获得的最大分数。

【举例】

arr = {3,2,5}

  • 如果先打爆3,获得 3 ∗ 2 3*2 32;再打爆2,获得 2 ∗ 5 2*5 25;最后打爆5,获得5;最后总分21。
  • 如果先打爆3,获得 3 ∗ 2 3*2 32;再打爆5,获得 2 ∗ 5 2*5 25;最后打爆2,获得2;最后总分18。
  • 如果先打爆2,获得 3 ∗ 2 ∗ 5 3*2*5 325;再打爆3,获得 3 ∗ 5 3*5 35;最后打爆5,获得5;最后总分50。
  • 如果先打爆2,获得 3 ∗ 2 ∗ 5 3*2*5 325;再打爆5,获得 3 ∗ 5 3*5 35;最后打爆3,获得3;最后总分48。
  • 如果先打爆5,获得 2 ∗ 5 2*5 25;再打爆3,获得 3 ∗ 2 3*2 32;最后打爆2,获得2;最后总分18。
  • 如果先打爆5,获得 2 ∗ 5 2*5 25;再打爆2,获得 3 ∗ 2 3*2 32;最后打爆3,获得3;最后总分19。

返回能获得的最大分数为50。

1.2 思路分析

首先,很自然地想在 L L L R R R 范围上枚举第一个打爆的气球inf f(arr, L, R),但是这个局部尝试的方法不行,因为选择打爆的气球左右两边的气球情况是不知道的,但是两边的气球是会决定分数的。所以,一定是要加参数的。

注意,如果设计的函数的返回值只由可变参数决定,如果是,那就是一个无后效性问题,即可变参数一定,返回值就是确定的。而本题不符合,只给定 L L L R R R 参数的值,不能决定最后的得分。

接下来说解法:

依然设计函数 int f(int[] arr, int L, int R),返回值表示得到的最大得分,但是需要满足潜台词:的确是在 arr 的 [ L , R ] [L,R] [L,R] 范围上打爆气球,但是必须满足 arr 中 L − 1 L- 1 L1 位置的气球没爆, R + 1 R+1 R+1位置的气球没爆,才能调用该函数。

如此一来,以「最后一个被打爆的气球的位置」进行可能性的枚举。

举个例子:arr = [2, 3, 5, 2, 1]

可能性枚举:

  1. 0位置的气球最后打爆。那么之前打过的气球范围是[1, 4],也就是说在调用 f(1,4) 的时候是满足上述的条件的:可以认为0位置的气球没爆,5位置的气球没爆。
  2. 1位置的气球最后打爆。那么可以调用 f(0,0) 得到一个分数,调用该函数时可认为此时-1位置的气球没爆,分数为1,1位置的气球也没爆,分数为3,此时就得到了左边的得分 f ( 0 , 0 ) f(0,0) f(0,0);然后调用 f(2, 4) 得到一个分数;最终的得分就是 f ( 0 , 0 ) + f ( 2 , 4 ) + 1 ∗ a r r [ 1 ] ∗ 1 f(0,0) + f(2,4) + 1 * arr[1] * 1 f(0,0)+f(2,4)+1arr[1]1,最后的 1 ∗ a r r [ 1 ] ∗ 1 1 * arr[1] * 1 1arr[1]1 可理解为当左边没有气球的时候分数为1,右边没有气球的时候分数也为1,最后打爆1位置的气球得分就是 1 ∗ a r r [ 1 ] ∗ 1 1 * arr[1] * 1 1arr[1]1

因为涉及到越界问题,可以将原数组 arr = [2, 3, 5, 2, 1] 处理成 arr‘ = [1, 2, 3, 5, 2, 1, 0],也就是在最前面和最后面都添加一个1,那么「针对 arr 数组的 f(0,4) 的调用」等价于「调用 arr'f(1, 5)」。

1.3 代码实现

  • 暴力递归
public class BurstBalloons {
	public static int maxCoins(int[] arr) {
		//原始数组:[3, 2, 1, 3]
		//处理得到的help数组:[1, 3, 2, 1, 3, 1]
		int N = arr.length;
		int[] help = new int[N + 2];
		
		for (int i = 0; i < N; i++) {
			help[i + 1] = arr[i];
		}
		help[0] = 1;
		help[N + 1] = 1;
		
		return func(help, 1, N);
	}
	
	//暴力递归解法:超时
	//L-1位置和 R+1位置永不越界,且 L-1位置和R+1位置一定没爆
	//返回 arr 的 L到R范围上打爆所有气球,最大的得分
	public static int func(int[] arr, int L, int R) {
		if (L == R) { //只剩一个气球,打爆它
			//满足潜台词:左边的气球和右边的气球一定没爆
			return arr[L - 1] * arr[L] * arr[R + 1]; 
		}
		//尝试每一种情况,在L到R范围进行尝试:最后打爆的气球是什么位置
		//可能性1:L位置的气球最后打爆
		//调用func(arr, L + 1, R)这个子过程的时候,已经满足了潜台词,L位置和R+1位置没爆
		//打最后一个L位置的气球时,此时它的左边没打爆的是L-1位置,右边没打爆的是R+1位置
		int max = func(arr, L + 1, R) + arr[L - 1] * arr[L] * arr[R + 1];
		
		//可能性2:R位置的气球最后打爆
		max = Math.max(max, func(arr, L, R - 1) + arr[L - 1] * arr[R] * arr[R + 1])//可能性3:尝试(L,R)范围的所有位置
		for (int i = L + 1; i < R; i++) {
			// i位置的气球最后打爆
			int left = func(arr, L, i - 1);
			int right = func(arr, i + 1, R);
			int last = arr[L - 1] * arr[i] * arr[R + 1];
			int cur = left + right + last;
			max = Math.max(max, cur);
		}
		return max;
	} 	
}
  • 动态规划
public class BurstBalloons {
	public static int maxCoins(int[] arr) {
		if (arr == null || arr.length == 0) {
			return 0;
		}
		if (arr.length == 1) {
			return arr[0];
		}
		int N = arr.length;
		int[] help = new int[N + 2];
		help[0] = 1;
		help[N + 1] = 1;
		for (int i = 0; i < N; i++) {
			help[i + 1] = arr[i];
		}
		int[][] dp = new int[N + 2][N + 2];
		for (int i = 1; i <= N; i++) {
			dp[i][i] = help[i - 1] * help[i] * help[i + 1];
		}
		for (int L = N; L >= 1; L--) {
			for (int R = L + 1; R <= N; R++) {
				int ans = help[L - 1] * help[L] * help[R + 1] + dp[L + 1][R];
				ans = Math.max(ans, help[L - 1] * help[R] * help[R + 1] + dp[L][R - 1]);
				for (int i = L + 1; i < R; i++) {
					ans = Math.max(ans, help[L - 1] * help[i] * help[R + 1] + dp[L][i - 1] + dp[i + 1][R]);
				}
				dp[L][R] = ans;
			}
		}
		return dp[1][N];
	}
}

1.4 启示

本题尝试的方法是潜台词+最后一个打爆的气球,如果题目的得分是求被打爆气球的左边离它最近的打爆的气球 和 右边离它最近的打爆的气球最终得分,那就枚举「第一个打爆的气球位置」

启示:无论什么尝试的方法,都要满足可变参数的复杂度不要突破整型以上,通常有两种技巧:

  1. 设计潜台词。当设计的函数满足某些潜台词的情况下,有可能就不用管某些我需要的外部信息,比如本题中在规定了潜台词「 L − 1 L-1 L1位置没爆, R + 1 R+1 R+1位置没爆」的前提下,就可以省下左侧信息的具体情况和右侧信息的具体情况
  2. 从尝试入手。可能性的展开方式。

2、Leetcode 546.移除盒子(困难)

2.1 题目描述

给出一些不同颜色的盒子 boxes ,盒子的颜色由不同的正数表示。

你将经过若干轮操作去去掉盒子,直到所有的盒子都去掉为止。每一轮你可以移除具有相同颜色的连续 k 个盒子(k >= 1),这样一轮之后你将得到 k * k 个积分。

返回 你能获得的最大积分和

示例1:

输入:boxes = [1,3,2,2,2,3,4,3,1]
输出:23
解释:
[1, 3, 2, 2, 2, 3, 4, 3, 1] 
----> [1, 3, 3, 4, 3, 1] (3*3=9 分) 
----> [1, 3, 3, 3, 1] (1*1=1 分) 
----> [1, 1] (3*3=9 分) 
----> [] (2*2=4 分)

示例2:

输入:boxes = [1,1,1]
输出:9

示例3:

输入:boxes = [1]
输出:1

2.2 思路分析

暴力递归

L L L R R R 范围上移除盒子,但是不给这个范围左边和右边的信息是无法处理的,所以还是面对着一个外部信息如何简化的过程。

一种猜法:设计一个函数 int f(int[] arr, int L, int R, int k),表示在 arr 的 L 到 R 范围上移除盒子,且在 arr 的左边有 k 个和 arr[L] 一样的数紧贴着 arr,在这种情况下移除 arr 的 L- k 到 R 范围上的盒子的最大积分是多少?

如何展开可能性呢?

举个例子:
在这里插入图片描述
对于该例子,就是在调用 f ( a r r , 21 , 31 , 5 ) f(arr, 21, 31, 5) f(arr,21,31,5)

按理来说,对于前面的5个1,可以直接将这5个1单独消掉,不和紧挨着的后面的1一起消除,得分 5 × 5 5 \times 5 5×5,然后调用 f ( a r r , 21 , 31 , 0 ) f(arr, 21, 31, 0) f(arr,21,31,0)。但是通常这种可能性不考虑,因为连续的相同的数越多,一起消除得分就越高。

那么对于有效的可能性:

  1. 调用 f ( a r r , 22 , 31 , 6 ) f(arr, 22, 31, 6) f(arr,22,31,6),也就是将前面的5个1和21位置的1合在一起;
  2. 调用 f ( a r r , 23 , 31 , 7 ) f(arr, 23, 31, 7) f(arr,23,31,7),将前面的6个1和22位置的1合在一起;
  3. … …

也就是说,要决定5个1是和后面的哪个位置一起消掉:

  • 如果5个1和21位置的1一起消掉,则后面的过程调用 f ( a r r , 22 , 31 , 6 ) f(arr,22, 31, 6) f(arr,22,31,6)
  • 如果5个1和22位置的1一起消掉,那么对于21位置的1,就要调用 f ( a r r , 21 , 21 , 0 ) f(arr, 21, 21, 0) f(arr,21,21,0),因为它自己单独消掉,5个1贴着22位置的1,所以还要调用 f ( a r r , 22 , 31 , 5 ) f(arr, 22, 31, 5) f(arr,22,31,5),整体就是 f ( a r r , 21 , 21 , 0 ) + f ( a r r , 22 , 31 , 5 ) f(arr, 21, 21, 0) + f(arr, 22, 31, 5) f(arr,21,21,0)+f(arr,22,31,5)
  • 如果选择和 23 位置的 1 一起消除,那么就是调用 f ( a r r , 21 , 22 , 0 ) + f ( a r r , 23 , 31 , 5 ) f(arr, 21, 22, 0) + f(arr, 23, 31, 5) f(arr,21,22,0)+f(arr,23,31,5)
  • 这5个1还可以和 26 位置的1 一起消,也可以跟着27位置的1一起小,还可以跟着 28 位置的1 一起消,跟着31位置的1一起消。将中间范围的数消除后就可以促成这种局面。

也就是说 可能性的枚举就是 5 个 1 和 (21, 31) 范围上的哪个位置的 1 一起消除,要么自己消除,要么和其他位置一起消除。

代码实现:

//在arr的L到R范围上消除,且前面紧跟着k个arr[L]这个数,返回所有东西都消掉的最大得分
public class RemoveBoxes {
	public static int func1(int[] arr, int L, int R, int K) {
		//无效范围
		if (L > Rreturn 0;
		
		//K个arr[L]值和L位置的值一起消掉,一共K+1个数一起消掉的
		//后续过程就是消除 L+1 到 R 范围上的数,前面一个数都没有了
		int ans = func1(arr, L + 1, R, 0) + (K + 1) * (K + 1);
		
		//前面的K个arr[L] 和 L 位置的数合并了,现在一共有K+1个arr[L]这个数
		for (int i = L + 1; i <= R; i++) {
			//如果i位置的数和arr[L]相等,则K+1个数和i位置的数一起消掉,先消除L+1到i-1范围上的数,然后K+1个数就和i位置的数挨着了
			//即是在枚举L+1到R范围上所有和arr[L]相等的位置
			if (arr[i] == arr[L]) { 
				ans = Math.max(ans, func1(arr, L + 1, i - 1, 0) + func1(arr, i, R, K + 1));
			}
		}
		return ans;
	}
}

记忆化搜索版本:

public class RemoveBoxes {
	public static int removeBoxes1(int[] boxes) {
		int N = boxes.length;
		int[][][] dp = new int[N][N][N];
		int ans = process1(boxes, 0, N - 1, 0, dp);
		return ans;
	}

	public static int process1(int[] boxes, int L, int R, int K, int[][][] dp) {
		if (L > R) {
			return 0;
		}
		if (dp[L][R][K] > 0) {
			return dp[L][R][K];
		}
		int ans = process1(boxes, L + 1, R, 0, dp) + (K + 1) * (K + 1);
		for (int i = L + 1; i <= R; i++) {
			if (boxes[i] == boxes[L]) {
				ans = Math.max(ans, process1(boxes, L + 1, i - 1, 0, dp) + process1(boxes, i, R, K + 1, dp));
			}
		}
		dp[L][R][K] = ans;
		return ans;
	}
}

优化版本:只能优化常数项

在这里插入图片描述
如果 L 到 R 范围上有连续的1,就将 L 移动到最后一个 1 的位置处,则此时:
在这里插入图片描述
也就是说,如果开头有相同的,只保留一个,若中间有连续相同的则只尝试其中的第一个。也就是收,上述开头的 8 个1 只需要尝试和 26 位置的 1 一起消掉即可,因为26/27/28位置是连续的1,当来到该范围的时候,会自动去做最好的合并。

代码实现:

public class RemoveBoxes {
	public static int removeBoxes2(int[] boxes) {
		int N = boxes.length;
		int[][][] dp = new int[N][N][N];
		int ans = process2(boxes, 0, N - 1, 0, dp);
		return ans;
	}

	public static int process2(int[] boxes, int L, int R, int K, int[][][] dp) {
		if (L > R) {
			return 0;
		}
		if (dp[L][R][K] > 0) {
			return dp[L][R][K];
		}
		// 找到开头,
		// 1,1,1,1,1,5
		// 3 4 5 6 7 8   -> 下标
		//         !
		int last = L; //相同前缀中的最后一个位置
		while (last + 1 <= R && boxes[last + 1] == boxes[L]) {
			last++;
		}
		// K个1     (K + last - L) last
		int pre = K + last - L; //此时在last这个位置之前连续的arr[L]这个数的个数,即last之前相同的数合并到一起了,此时last这个位置的值还没和前面的(K+last-1)个数合在一起
		//可能性1:pre个1和last这个位置的1一起消掉
		int ans = (pre + 1) * (pre + 1) + process2(boxes, last + 1, R, 0, dp);
		//可能性2:和其他位置的数一起消掉
		//之所以从last+2位置开始尝试,是因为已经明确知道last+1位置的数不是arr[l]
		for (int i = last + 2; i <= R; i++) {
			if (boxes[i] == boxes[L] && boxes[i - 1] != boxes[L]) { //找到第一个和arr[l]相等的位置,连续的情况也只需要找到第一个位置
				ans = Math.max(ans, process2(boxes, last + 1, i - 1, 0, dp) + process2(boxes, i, R, pre + 1, dp));
			}
		}
		dp[L][R][K] = ans;
		return ans;
	}
}

2.3 代码实现

// 本题测试链接 : https://leetcode.com/problems/remove-boxes/
public class RemoveBoxes {
	//暴力递归版本
	// arr[L...R]消除,而且前面跟着K个arr[L]这个数
	// 返回:所有东西都消掉,最大得分
	public static int func1(int[] arr, int L, int R, int K) {
		if (L > R) {
			return 0;
		}
		int ans = func1(arr, L + 1, R, 0) + (K + 1) * (K + 1);
		
		// 前面的K个X,和arr[L]数,合在一起了,现在有K+1个arr[L]位置的数
		for (int i = L + 1; i <= R; i++) {
			if (arr[i] == arr[L]) {
				ans = Math.max(ans, func1(arr, L + 1, i - 1, 0) + func1(arr, i, R, K + 1));
			}
		}
		return ans;
	}
	
	//记忆化搜索版本
	public static int removeBoxes1(int[] boxes) {
		int N = boxes.length;
		int[][][] dp = new int[N][N][N];
		int ans = process1(boxes, 0, N - 1, 0, dp);
		return ans;
	}

	public static int process1(int[] boxes, int L, int R, int K, int[][][] dp) {
		if (L > R) {
			return 0;
		}
		if (dp[L][R][K] > 0) {
			return dp[L][R][K];
		}
		int ans = process1(boxes, L + 1, R, 0, dp) + (K + 1) * (K + 1);
		for (int i = L + 1; i <= R; i++) {
			if (boxes[i] == boxes[L]) {
				ans = Math.max(ans, process1(boxes, L + 1, i - 1, 0, dp) + process1(boxes, i, R, K + 1, dp));
			}
		}
		dp[L][R][K] = ans;
		return ans;
	}
	
	//动态规划版本
	public static int removeBoxes2(int[] boxes) {
		int N = boxes.length;
		int[][][] dp = new int[N][N][N];
		int ans = process2(boxes, 0, N - 1, 0, dp);
		return ans;
	}

	public static int process2(int[] boxes, int L, int R, int K, int[][][] dp) {
		if (L > R) {
			return 0;
		}
		if (dp[L][R][K] > 0) {
			return dp[L][R][K];
		}
		// 找到开头,
		// 1,1,1,1,1,5
		// 3 4 5 6 7 8
		//         !
		int last = L;
		while (last + 1 <= R && boxes[last + 1] == boxes[L]) {
			last++;
		}
		// K个1     (K + last - L) last
		int pre = K + last - L;
		int ans = (pre + 1) * (pre + 1) + process2(boxes, last + 1, R, 0, dp);
		for (int i = last + 2; i <= R; i++) {
			if (boxes[i] == boxes[L] && boxes[i - 1] != boxes[L]) {
				ans = Math.max(ans, process2(boxes, last + 1, i - 1, 0, dp) + process2(boxes, i, R, pre + 1, dp));
			}
		}
		dp[L][R][K] = ans;
		return ans;
	}
}

3、消除字符

3.1 题目描述

如果一个字符相邻的位置没有相同字符,那么这个位置的字符出现不能被消除。比如:“ab”,其中a和b都不能被消除。

如果一个字符相邻的位置有相同字符,就可以一起消除。比如:“abbbc”,中间一串的b是可以被消除的,消除之后剩下“ac”。某些字符如果消除了,剩下的字符认为重新靠在一起。

给定一个字符串,你可以决定每一步消除的顺序,目标是请尽可能多的消除字符,返回最少的剩余字符数量。

示例1:

"aacca",
如果先消掉最左侧的"aa",那么将剩下"cca";
然后把"cc"消掉,剩下的"a"将无法再消除,返回1。

如果先消掉中间的"cc",那么将剩下"aaa";
最后都消掉就一个字符也不剩了,返回0,这才是最优解。

示例2:

"baaccabb"
如果先消除最左侧的两个a,剩下"bccabb";
如果再消除最左侧的两个c,剩下"babb";
最后消除最右侧的两个b,剩下"ba"无法再消除,返回2。

而最优策略是:
先消除中间的两个c,剩下"baaabb";
再消除中间的三个a,剩下"bbb";
最后消除三个b,不留下任何字符,返回0,这才是最优解

3.2 思路分析

和“移除盒子”问题类似,设计一个函数 int f(L, R, has),表示要在L到R范围上消掉字符,但是前面是否有arr[L]位置的字符跟着,这种情况下最少剩几个字符。

举例:
“aaaabckaaad”,如果在该字符串前面没有a跟着,则先找到前缀中最后一个a的位置是L+3,后续过程是 f ( L + 3 , R , t r u e ) f(L+3, R, true) f(L+3,R,true),表示在L+3到R范围上,且前面有a跟着的情况下,消掉字符最少剩几个。

“abckfbckaaad”,如果该字符串前面没有a跟着,此时就变成了L位置的a要和什么位置一起消掉,如果它不消,结果就是 1 + f(L+1, R, false),其中的1就是这个不消的a;如果要消a,那就是先将L位置的a和后面的第一个a之间的字符"bckfbck"消掉,如果它能消成0,那L位置的a就可以和后续的a一起消,如果消不成0,那L位置的a就只能剩下。

3.3 代码实现

public class DeleteAdjacentSameCharacter {

	// 暴力解
	public static int restMin1(String s) {
		if (s == null) {
			return 0;
		}
		if (s.length() < 2) {
			return s.length();
		}
		int minLen = s.length();
		for (int L = 0; L < s.length(); L++) {
			for (int R = L + 1; R < s.length(); R++) {
				if (canDelete(s.substring(L, R + 1))) {
					minLen = Math.min(minLen, restMin1(s.substring(0, L) + s.substring(R + 1, s.length())));
				}
			}
		}
		return minLen;
	}

	public static boolean canDelete(String s) {
		char[] str = s.toCharArray();
		for (int i = 1; i < str.length; i++) {
			if (str[i - 1] != str[i]) {
				return false;
			}
		}
		return true;
	}

	// 优良尝试的暴力递归版本
	public static int restMin2(String s) {
		if (s == null) {
			return 0;
		}
		if (s.length() < 2) {
			return s.length();
		}
		char[] str = s.toCharArray();
		return process(str, 0, str.length - 1, false);
	}

	// str[L...R] 前面有没有跟着[L]字符,has表示, T 有 F 无
	// L,R,has
	// 最少能剩多少字符,消不了
	public static int process(char[] str, int L, int R, boolean has) {
		if (L > R) {
			return 0;
		}
		if (L == R) { //只剩一个字符,取决于前面是否有字符跟着
			return has ? 0 : 1;
		}
		int index = L;
		int K = has ? 1 : 0;
		while (index <= R && str[index] == str[L]) {
			K++;
			index++;
		}
		// index表示,第一个不是[L]字符的位置
		// K 只会 >=1
		// K > 1表示开始的一串能全部消掉
		int way1 = (K > 1 ? 0 : 1) + process(str, index, R, false);
		int way2 = Integer.MAX_VALUE;
		for (int split = index; split <= R; split++) {
			//找到后续的和[L]字符的位置相同的中的位置split,如果是连续的就找第一个
			if (str[split] == str[L] && str[split] != str[split - 1]) { 
				//如果开始连续的字符和split位置之间的字符能完全消掉,剩0个字符,则开始连续的字符和split的位置的字符一起消
				//否则不一起消,因为没法一起
				if (process(str, index, split - 1, false) == 0) { 
					way2 = Math.min(way2, process(str, split, R, K != 0));
				}
			}
		}
		return Math.min(way1, way2);
	}

	// 优良尝试的动态规划版本
	public static int restMin3(String s) {
		if (s == null) {
			return 0;
		}
		if (s.length() < 2) {
			return s.length();
		}
		char[] str = s.toCharArray();
		int N = str.length;
		int[][][] dp = new int[N][N][2];
		for (int i = 0; i < N; i++) {
			for (int j = 0; j < N; j++) {
				for (int k = 0; k < 2; k++) {
					dp[i][j][k] = -1;
				}
			}
		}
		return dpProcess(str, 0, N - 1, false, dp);
	}
	
	//虽然有三个可变参数,但是has是布尔类型,所以可以认为has=true的时候有一张表,has = false的时候有一张表
	//两张二维表
	public static int dpProcess(char[] str, int L, int R, boolean has, int[][][] dp) {
		if (L > R) {
			return 0;
		}
		int K = has ? 1 : 0;
		if (dp[L][R][K] != -1) {
			return dp[L][R][K];
		}
		int ans = 0;
		if (L == R) {
			ans = (K == 0 ? 1 : 0);
		} else {
			int index = L;
			int all = K;
			while (index <= R && str[index] == str[L]) {
				all++;
				index++;
			}
			int way1 = (all > 1 ? 0 : 1) + dpProcess(str, index, R, false, dp);
			int way2 = Integer.MAX_VALUE;
			for (int split = index; split <= R; split++) {
				if (str[split] == str[L] && str[split] != str[split - 1]) {
					if (dpProcess(str, index, split - 1, false, dp) == 0) {
						way2 = Math.min(way2, dpProcess(str, split, R, all > 0, dp));
					}
				}
			}
			ans = Math.min(way1, way2);
		}
		dp[L][R][K] = ans;
		return ans;
	}

	public static String randomString(int len, int variety) {
		char[] str = new char[len];
		for (int i = 0; i < len; i++) {
			str[i] = (char) ((int) (Math.random() * variety) + 'a');
		}
		return String.valueOf(str);
	}

	public static void main(String[] args) {
		int maxLen = 16;
		int variety = 3;
		int testTime = 100000;
		System.out.println("测试开始");
		for (int i = 0; i < testTime; i++) {
			int len = (int) (Math.random() * maxLen);
			String str = randomString(len, variety);
			int ans1 = restMin1(str);
			int ans2 = restMin2(str);
			int ans3 = restMin3(str);
			if (ans1 != ans2 || ans1 != ans3) {
				System.out.println(str);
				System.out.println(ans1);
				System.out.println(ans2);
				System.out.println(ans3);
				System.out.println("出错了!");
				break;
			}
		}
		System.out.println("测试结束");
	}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/459871.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ARM buildroot 的引入

一、X210 的 bsp 介绍 1、嵌入式 linux 产品的 bsp 介绍 (1) 大部分的 ARM 架构的 linux 平台的 bsp 的内容和结构都是相似的。 (2) bsp 一般是芯片厂家/板卡厂家提供的。 2、X210 的 linuxQT bsp 整体介绍 (1) tslib_x210_qtopia.tgz 是用来支持 QT 的触摸屏操作的应用层库。…

操作系统笔记——绪论

第一章绪论 1.1 操作系统的基本概念 1.1.1计算机硬件的基本组成 &#xff08;冯 ~诺伊曼模型&#xff09; &#xff0c;由运算器&#xff0c;存储器&#xff0c;控制器&#xff0c;输入设备&#xff0c;输出设备组成。 引入操作系统的目的&#xff1a;提供一个计算机用户与…

【2. 初学ROS,年轻人的第一个Node节点】

【2. 初学ROS&#xff0c;年轻人的第一个Node节点】 1. 工作空间设置2. 创建Package3. 回访依赖包4. 创建Node节点5. 源码编译6. 运行Node节点7. Node节点完善8. 总结 本教程是B站阿杰视频的笔记 视频地址&#xff1a;https://www.bilibili.com/video/BV1nG411V7HW 超声波传感器…

streamx平台部署

一. streamx介绍 StreamPark 总体组件栈架构如下&#xff0c; 由 streampark-core 和 streampark-console 两个大的部分组成 , streampark-console 是一个非常重要的模块, 定位是一个综合实时数据平台&#xff0c;流式数仓平台, 低代码 ( Low Code ), Flink & Spark 任务托…

【PWN刷题__ret2shellcode】[HNCTF 2022 Week1]ret2shellcode

本蒟蒻的ret2shellcode的开篇之作&#xff01; 第一次实战ret2shellcode&#xff0c;该类型的简单题但是也研究了很久! 目录 前言 一、checksec查看二进制文件​ 二、查找后门函数 三、IDA反汇编 bss段 mprotect()函数 四、GDB调试 GDB基本的一些用法 偏移量计算 五…

OpenSearch图搜图、文搜图向量检索最佳实践

一、向量检索介绍 1.1 多模态信息的典型特点-非结构化 信息可以被划分为两大类&#xff1a;当信息能够用数据或统一的结构加以表示&#xff0c;称之为结构化数据&#xff1b;当信息无法用数字或统一的结构表示&#xff0c;称之为非结构化数据。非结构数据与结构化数据相比较而…

Rockchip芯片单板适配OpenHarmony的方法

Rockchip芯片单板适配OpenHarmony的方法 1 整体思路 OpenHarmony是一个上层用户操作系统&#xff0c;在设计上希望兼容不同的底层系统。针对于L2的Linux标准设备&#xff0c;OpenHarmony对Linux、Uboot等底层系统没有太多的依赖&#xff0c;并且在驱动方面&#xff0c;HDF也兼…

蓝桥杯省一经验+考试流程+技巧分享

今年拿了省一&#xff0c;开心&#xff0c;我渡梦酒也可以拿奖奖啦。 我对整体参赛流程还是比较熟悉了&#xff0c;给大家留下一点值得参考的东西~。 这篇纯纯经验和技巧分享&#xff0c;请放心食用~ 目录 考试流程 考试代码怎么提交 考完结束需要做什么 做题小技巧&#xf…

适合学生党的蓝牙耳机品牌有哪些?性价比高的无线耳机推荐

相较于有线耳机&#xff0c;蓝牙耳机的受欢迎程度可谓是越来越高&#xff0c;当然&#xff0c;这也离不开部分手机取消耳机孔的设计。最近看到很多网友问&#xff0c;适合学生党的蓝牙耳机品牌有哪些&#xff1f;针对这个问题&#xff0c;我来给大家推荐几款性价比高的无线耳机…

VR全景摄影,全景VR展示模式

目前&#xff0c;全景概念已经被大众熟知&#xff0c;很多行业尤其是房产、汽车等已经开始大批量使用全景展示的方式提高获客率和推广率。VR全景摄影以全景摄影技术为基础&#xff0c;结合虚拟现实技术&#xff0c;可以让用户身临其境&#xff0c;沉浸式地感受虚拟环境。 一、V…

【 Spring AOP 】

文章目录 一、什么是 Spring AOP&#xff1f;二、为什要⽤ AOP&#xff1f;三、AOP 的组成四、Spring AOP 的实现五、Spring AOP 实现原理 一、什么是 Spring AOP&#xff1f; AOP&#xff08;Aspect Oriented Programming&#xff09;&#xff1a;⾯向切⾯编程&#xff0c;它…

【Linux常见指令以及权限理解】基本指令(2)

写在前面 今天我们继续学习Linux的基本指令&#xff0c; 这里是上一篇博客的链接&#xff1a;http://t.csdn.cn/9AgHP 接下来我会继续讲解Linux指令相关内容。 目录 写在前面 1. man 描述&#xff1a; 用法&#xff1a; 例子&#xff1a; 例1&#xff1a; 例2&#…

MC9S12G128开发板—实现按键发送CAN报文指示小车移动功能

实验环境&#xff1a;MC9S12G128开发板 基本功能&#xff1a;控制开发板上的按键&#xff0c;模拟车辆移动的上下左右四个方位&#xff0c;通过can通信告诉上位机界面&#xff0c;车辆轨迹的移动方位。 1. 1939报文发送的示例代码 MC9S12G128开发板1939协议发送can报文数据的…

redmine问题跟踪系统4.1版本一键安装包下载

很好用的项目管理&#xff0c;缺陷跟踪系统&#xff0c;开源免费使用 Version 4.1.1-4 2020-08-31 由 redmineplugins.cn Admin 在 超过 2 年 之前添加 Version 4.1.1-4 2020-08-31 Maintenance releaseUpdated Apache to 2.4.46Updated Git to 2.28.0Updated PHP to 7.3.21U…

初识uniapp

创建小程序 依次点击HBuilderx 左上方的按钮&#xff1a;文件->新建->项目 然后打开该界面&#xff0c;输入项目名称&#xff0c;点击 浏览 按钮&#xff0c;可以选择项目保存的目录&#xff0c;这些完成后点击 创建 按钮就好了 比如小颖的项目名叫 &#xff1a;test-y…

基于一致性的半监督学习用于诊断x线片分类

文章目录 Consistency-Based Semi-supervised Evidential Active Learning for Diagnostic Radiograph Classification摘要方法Evidential-based Semi-supervised LearningEvidential-based Active Learning Consistency-Based Semi-supervised Evidential Active Learning for…

Java+Angular开发的医院信息管理系统源码,系统部署于云端,支持多租户

云HIS系统源码&#xff0c;采用云端SaaS服务的方式提供 基于云计算技术的B/S架构的云HIS系统源码&#xff0c;采用云端SaaS服务的方式提供&#xff0c;使用用户通过浏览器即能访问&#xff0c;无需关注系统的部署、维护、升级等问题&#xff0c;系统充分考虑了模板化、配置化、…

第四届CECC中国计算机教育大会召开,飞桨持续加码产教融合教育新生态

‍‍大模型作为人工智能发展的新方向&#xff0c;对人才的需求和培养带来了新挑战。4月21日至22日&#xff0c;以“新时代 新挑战 新任务”为主题的第四届中国计算机教育大会&#xff08;CECC&#xff09;在厦门召开&#xff0c;飞桨承办“人工智能与大模型”论坛同期举办。立足…

【Python安卓开发】BeeWare框架:环境准备

&#x1f4ad; 写在前面&#xff1a;我们假定读者已经安装好了 3.8 版本后的 Python&#xff0c;并且安装好了依赖项&#xff0c;在 Windows 上构建 BeeWare 应用程序需要 Git&#xff0c;你可以可以从 git-scm.org 网站下载。安装完毕后重新启动 cmd&#xff0c;然后就可以准备…

Compiler- 循环展开

循环展开不仅在编译原理中有涉及到&#xff0c;笔者记得在CSAPP里面也提到了这种优化方法。 话不多说&#xff0c;我们先来看个例子。 int loop(int a) {int result 0;for(int i 0; i < a; i){result i;}return result; }int loop1(int a) {int result 0;int len a/2…