身为小白的我,最近再刷动态规划的题目。一开始刷题也是跟着题解那样,直击状态转移方程。但有时候这个状态转移方程并非那么好找,而且在不同条件情况下,我们的状态转移方程也得随机条件而改变。所以,最近刷这类动态规划题,让我感到很吃力。但是通过一定题量还是能总结出一定的规律,下面让我用一道例题和大家分享一下我的学习成果。
- 问题描述
给定一个只由 0(假)、1(真)、&(逻辑与)、|(逻辑或)和^(异或)五种字符组成 的字符串express,再给定一个布尔值desired。返回express能有多少种组合 方式,可以达到desired的结果。
【举例】
express=“1^0|0|1”,desired=false
只有 1^((0|0)|1)和 1^(0|(0|1))的组合可以得到 false,返回 2 。
express=“1”,desired=false
无组合则可以得到false,返回0
看到这类大题,相信有些人和我一样被这段文字直接劝退。但是相信我,因为一开始我也是咬牙坚持住的,既然我可以,所以你们也行,让我们进入主题。
首先,这道题是通过运算符与数字,最终给一个true or false ,返回组合数。
我们先想想有什么样的情况才满足有true,false的值。
- 字符串要满足"数字+运算符+数字+运算符+。。。。。";
- 第一位必须要有数字;并且是间隔出现。
- 运算符也是间隔出现;
通过这一简单的分析,我们已经解决一小部分问题。
接着 解题思路:
既然要返回true 或 false 的组合数。那么是不是需要考虑到运算符的不同,对应判断对或错的情况不同,这就导致状态转移方程不同了。
这时候我想引入一下,其实动态规划就是一种(记忆化)暴力递归。两者其实关联紧密的。
为此,这题我们先从暴力递归开始分析。
我们是如何知道true 或 false?
当然是从运算符的左右两边对应的值。
true:
- &:满足左右两边都是1;
- | : 满足一边是1即可;
- ^:满足两边不同即可;
这边我只列举true的情况,false相信大家都会。
所以我们遍历字符串获取运算符,我们只需要理会左右两边的值对应运算符是题目所要的结果对应是否为一种组合数。
图解:
我们把正常情况考虑好了,紧着递归的特殊情况,终止条件就是当left==right
此时,只有当数组的对应下标为left为数值才可以返回true 或 false。这样一来递归的大体思路可以开始编写代码了。
public static boolean isOK(char[] arr) {
if ((arr.length & 1) == 0)
return false;
for (int i = 1; i < arr.length; i += 2) {
if (arr[i] == '0' || arr[i] == '1') {
return false;
}
}
for (int i = 0; i < arr.length; i += 2) {
if (arr[i] == '&' || arr[i] == '|' || arr[i] == '^') {
return false;
}
}
return true;
}
public static int fun1(String s, boolean desired) {
if (s == null || s.equals(""))
return 0;
char[] arr = s.toCharArray();
if (!isOK(arr)) {
return 0;
}
return desired(arr, desired, 0, arr.length - 1);
}
public static int desired(char[] p, boolean desired, int L, int R) {
if (L == R) {
if (p[L] == '1') {
return desired ? 1 : 0;
} else {
return desired ? 0 : 1;
}
}
int res = 0;
if (desired) {
for (int i = L + 1; i < R; i += 2) {
switch (p[i]) {
case '&':
res += desired(p, true, L, i - 1) * desired(p, true, i + 1, R);
break;
case '|':
res += desired(p, true, L, i - 1) * desired(p, false, i + 1, R);
res += desired(p, false, L, i - 1) * desired(p, true, i + 1, R);
res += desired(p, true, L, i - 1) * desired(p, true, i + 1, R);
break;
case '^':
res += desired(p, true, L, i - 1) * desired(p, false, i + 1, R);
res += desired(p, false, L, i - 1) * desired(p, true, i + 1, R);
break;
}
}
} else {
for (int i = L + 1; i < R; i += 2) {
switch (p[i]) {
case '&':
res += desired(p, false, L, i - 1) * desired(p, true, i + 1, R);
res += desired(p, true, L, i - 1) * desired(p, false, i + 1, R);
res += desired(p, false, L, i - 1) * desired(p, false, i + 1, R);
break;
case '|':
res += desired(p, false, L, i - 1) * desired(p, false, i + 1, R);
break;
case '^':
res += desired(p, true, L, i - 1) * desired(p, true, i + 1, R);
res += desired(p, false, L, i - 1) * desired(p, false, i + 1, R);
break;
}
}
}
return res;
}
动态规划解:
其实就是绘制图表,填写数据。我们暴力递归是为了获取整段0-arr.length-1结果为true 或 false 的组合数。所以我们最终递归要返回的值 就是根据题目的true 或者 false 二维数组t[0][t.length-1] 或f[0][f.length];
所以动态规划应该自下而上,自左而右。
然后将递归的情况代入双循环中根据情况而改数据;
对应代码:
public static int fun2(String express, boolean desired) {
if (express == null || express.equals("")) {
return 0;
}
char[] exp = express.toCharArray();
if (!isOK(exp)) {
return 0;
}
int[][] t = new int[exp.length][exp.length];
int[][] f = new int[exp.length][exp.length];
for (int i = 0; i < exp.length; i += 2) {
t[i][i] = exp[i] == '0' ? 0 : 1;
f[i][i] = exp[i] == '0' ? 1 : 0;
}
for (int i = exp.length - 3; i >= 0; i -= 2) {
for (int j = i + 2; j < exp.length; j += 2) {
for (int k = i + 1; k < j; k += 2) {
switch (exp[k]) {
case '&':
t[i][j] += t[i][k - 1] * t[k + 1][j];
f[i][j] += (t[i][k - 1] * f[k + 1][j]) + (f[i][k - 1] * t[k + 1][j])
+ (f[i][k - 1] * f[k + 1][j]);
break;
case '|':
f[i][j] += f[i][k - 1] * f[k + 1][j];
t[i][j] += (t[i][k - 1] * f[k + 1][j]) + (f[i][k - 1] * t[k + 1][j])
+ (t[i][k - 1] * t[k + 1][j]);
break;
case '^':
t[i][j] += (t[i][k - 1] * f[k + 1][j]) + (f[i][k - 1] * t[k + 1][j]);
f[i][j] += (t[i][k - 1] * t[k + 1][j]) + (f[i][k - 1] * f[k + 1][j]);
break;
}
}
}
}
return desired ? t[0][exp.length - 1] : f[0][exp.length - 1];
}
优化动态规划解:
public static int fun3(String express, boolean desired) {
if (express == null || express.equals("")) {
return 0;
}
char[] exp = express.toCharArray();
if (!isOK(exp)) {
return 0;
}
int[][] t = new int[exp.length][exp.length];
int[][] f = new int[exp.length][exp.length];
t[0][0] = exp[0] == '0' ? 0 : 1;
f[0][0] = exp[0] == '1' ? 0 : 1;
for (int i = 2; i < exp.length; i += 2) {
t[i][i] = exp[i] == '0' ? 0 : 1;
f[i][i] = exp[i] == '0' ? 1 : 0;
for (int j = i - 2; j >= 0; j -= 2) {
for (int k = j; k < i; k += 2) {
if (exp[k + 1] == '&') {
t[j][i] += t[j][k] * t[k + 2][i];
f[j][i] += (f[j][k] + t[j][k]) * f[k + 2][i] + f[j][k] * t[k + 2][i];
} else if (exp[k + 1] == '|') {
t[j][i] += (f[j][k] + t[j][k]) * t[k + 2][i] + t[j][k] * f[k + 2][i];
f[j][i] += f[j][k] * f[k + 2][i];
} else {
t[j][i] += f[j][k] * t[k + 2][i] + t[j][k] * f[k + 2][i];
f[j][i] += f[j][k] * f[k + 2][i] + t[j][k] * t[k + 2][i];
}
}
}
}
return desired ? t[0][exp.length - 1] : f[0][exp.length];
}
能坚持看到最后的应该,都是大佬。哈哈哈,可能有些优化没想到,希望大佬指点!!