题目一
当暴力递归该动态规划时,发现一些初始状态没有,可以回到原问题,将所需的初始状态推出来。另外,写出的递归代码一定要具有无后效性,即子问题的结果不会因为母问题结果而受到影响。
递归:大问题做决策1调小问题1、做决策2调小问题2…一定要保证大问题调小问题的所有影响,都体现在调小问题的参数上了,也就是要做到无后效性,大问题之前的状态不会影响到小问题。
原始串str,全部由小写字母组成、匹配串exp可以由小写字母、‘’、和‘.’组成,其中‘.’可以变成任意一个小写字母,但必须要变,‘’必须和前一个字符(包括‘.’)配合使用,可以将前一个字符变成任意个该字符(包括0个)。
f(str,exp,si,ei):str[si…]能否被exp[ei…]匹配。1) exp[ei+1]不是*–>如果当前位置不匹配,直接返回false,否则返回f(str,exp,si+1,ei+1);2) exp[ei+1]是*–>尝试将当前位置的字符和*变0…k个当前字符,后续只要有正确匹配的情况,当前结果就为true,否则为false。
代码实现:
bool isValid(string& s, string& e) {
for (int i = 0; i < s.length(); i++) {
if (s[i] < 'a' || s[i]>'z') {
return false;
}
}
for (int i = 0; i < e.length(); i++) {
if (((e[i] < 'a' || e[i]>'z')&&(e[i]!='*'&&e[i]!='.')) || (i == 0 && e[i] == '*') || (i + 1 < e.length() && (e[i] == '*' && e[i + 1] == '*'))) {
return false;
}
}
return true;
}
//f(i,j)表示:s[i..]和e[j..]能否匹配
//e[j]永远不要是*
bool f(string& s, string& e, int i, int j) {
if (j == e.length()) {
return i == s.length();
}
if (i == s.length()) {
if ((e.length() - j) % 2 == 1) {
return false;
}
for (int k = e.length() - 1; k > j; k -= 2) {
if (e[k] != '*' || e[k - 1] == '*') {
return false;
}
}
return true;
}
if (j == e.length() - 1 || e[j + 1] != '*') {
if ((s[i] != e[j] && e[j] != '.')) {
return false;
}
return f(s, e, i + 1, j + 1);
}
while (i<s.length()&&(s[i] == e[j] || e[j] == '.')) {
if (f(s, e, i, j + 2)) {
return true;
}
i++;
}
return f(s, e, i, j + 2);
}
bool isMatch(string& s, string& e) {
if (!isValid(s, e)) {
return false;
}
return f(s, e, 0, 0);
}
//改动态规划
bool isMatch2(string& s, string& e) {
if (!isValid(s, e)) {
return false;
}
vector<vector<bool>>dp(s.length() + 1, vector<bool>(e.length() + 1));
dp[s.length()][e.length()] = true;//最后一列
//最后一行
//a*b*c*..
for (int j = e.length() - 1; j >= 0; j--) {
if ((e.length() - j) % 2 == 1) {
dp[s.length()][j] = false;
}
else {
if (e[j] != '*' && e[j + 1] == '*') {
dp[s.length()][j] = true;
}
else {
break;//剩余的全部为false
}
}
}
//倒数第二列:e的最后一个字符能否和s[i..]匹配
//无后效性决定了最后一个字符如果为*,那么这个位置的值不会被使用,因此当作不是*处理
//表是为递归返回值服务的,不会递归到的位置,表中也不需要填,最后一个字符是*的情况,就属于不用填的情况
if (e[e.length() - 1] == s[s.length() - 1] || e[e.length() - 1] == '.') {
dp[s.length() - 1][e.length() - 1] = true;
}
//从下往上,从右往左
for (int i = dp.size() - 2; i >= 0; i--) {
for (int j = dp[0].size() - 3; j >= 0; j--) {
if (e[j + 1] != '*') {
if ((s[i] != e[j] && e[j] != '.')) {
dp[i][j] = false;
}
else {
dp[i][j] = dp[i + 1][j + 1];
}
}
else {
int i1 = i;
while (i1<s.length()&&(s[i1] == e[j] || e[j] == '.')) {
if (dp[i1][j + 2]) {
dp[i][j] = true;
break;
}
i1++;
}
if (dp[i][j] == false) {
dp[i][j] = dp[i1][j + 2];
}
}
}
}
return dp[0][0];
}
题目二
如果是累加和可以构建单调性,但是异或和很难,两个小数异或可能很大,两个大数异或可能很小。
不好的方法是求对任意两个位置都求他们之间异或和,返回其中的最大值。需要用到双层循环,第二层循环难以优化的原因是,不知道哪个前缀异或和与当前的前缀异或和异或得到的结果最大,使用前缀数,可以解决这一问题。具体如下:
将每个位置的前缀和依次添加到前缀树中,当遍历到arr[i]时,前缀树中存在i-1个前缀和,我们想从中找到谁与presum[i]异或的值最大,可以在前缀树中,尽量往能使异或结果高位为1的路径走。
如果是32位有符号整数,怎么做?最高位符号位,如果是1,更想遇到的是1,为0,更想遇到的是0;其他位还是尽量使高位为1。因为如果负数只能和正数异或,由于负数是用补码存储的,所以高位的1,变成原码就是0,此时就做到了,让结果尽可能大;如果负数可以和负数异或,那么符号位变成0,高位尽可能为1,也让结果尽可能大。
代码实现:
//方法一:不好的方法
int maxEor(vector<int>& arr) {
if (arr.size() == 0) {
return 0;
}
vector<int>presum(arr.size());
presum[0] = arr[0];
int res = arr[0];
//数据预处理
//前缀异或和
for (int i = 1; i < arr.size(); i++) {
presum[i] = presum[i - 1] ^ arr[i];
res = max(res, presum[i]);
}
//arr[i..j]的异或和=arr[0..j]的异或和^arr[0..i-1]的异或和
//原因是两个相等的数异或为0
for (int i = 1; i < arr.size(); i++) {
for (int j = i; j >= 1; j--) {
//第二层循环难以优化的原因是,不知道哪个前缀异或和与当前的前缀异或和异或得到的结果最大
//使用前缀数,可以解决这一问题
res = max(res, presum[i] ^ presum[j - 1]);
}
}
return res;
}
//方法二
class Node {
public:
vector<Node*> nexts;
Node() {
nexts.push_back(nullptr);
nexts.push_back(nullptr);
}
};
class NumTrie {
public:
Node* h;
NumTrie() {
this->h = new Node;
}
void add(int num) {
Node* cur = h;
for (int i = 31; i >= 0; i--) {
int path = ((num >> i) & 1);//回去i位的值0/1
cur->nexts[path] = (cur->nexts[path] == nullptr ? new Node : cur->nexts[path]);
cur = cur->nexts[path];
}
}
int maxeor(int eor) {
Node* cur = h;
int res = 0;
for (int i = 31; i >= 0; i--) {
int path = ((eor >> i) & 1);
int best = (i == 31 ? path : (path ^ 1));
best = (cur->nexts[best] != nullptr ? best : (best ^ 1));
res = (res | ((path ^ best) << i));
cur = cur->nexts[best];//bug出在这里,少了更新cur
}
return res;
}
};
int maxEor2(vector<int>& arr) {
if (arr.size() == 0) {
return 0;
}
NumTrie nt;
nt.add(0);
int res = arr[0];
int sum = 0;
for (int i = 0; i < arr.size(); i++) {
sum ^= arr[i];
res = max(res, nt.maxeor(sum));
nt.add(sum);
}
return res;
}
题目三
尝试1:假设arr[l…r]范围内的每个气球都尝试最先被打爆f(l.r),这样会遇到问题:当假设其中一个值是最先被打爆时,调用的子问题没法获得最接近他且没被打爆的气球。大问题对子问题的所有影响都要体现在调用参数上。
尝试2:假设arr[l…r]范围内的每个气球都尝试最后被打爆f(l.r),潜台词:l-1位置和r+1位置的气球一定没爆,这两个位置会调用f(l.r),说明此时正在尝试l-1位置/r+1位置最后被打爆;假设当前位置来到了i,即该位置的气球最后被打爆,那么他就可以调f(l,i-1)和f(i+1,r),当前获得分数位f(l,i-1)+f(i+1,r)+lir位置上的分数
代码实现:
int f(vector<int>& arr, int l, int r) {
if (l == r) {
return arr[l] * arr[l - 1] * arr[r + 1];
}
int res = max(arr[l] * arr[l - 1] * arr[r + 1] + f(arr, l + 1, r), arr[r] * arr[l - 1] * arr[r + 1] + f(arr, l, r - 1));
for (int i = l + 1; i < r; i++) {
res = max(res, arr[i] * arr[l - 1] * arr[r + 1] + f(arr, l, i - 1) + f(arr, i + 1, r));
}
return res;
}
int maxScore(vector<int>& arr) {
if (arr.size() == 0) {
return 0;
}
arr.resize(arr.size() + 2, 1);
for (int i = arr.size() - 2; i >= 0; i--) {
arr[i + 1] = arr[i];
}
arr[0] = 1;
return f(arr, 1, arr.size() - 2);
}
//改动态规划
int maxScore2(vector<int>& arr) {
int len = arr.size();
vector<vector<int>>dp(len - 1, vector<int>(len - 1));
for (int i = 1; i < dp.size(); i++) {
dp[i][i] = arr[i] * arr[i - 1] * arr[i + 1];
}
//从下往上,从左往右
for (int i = dp.size() - 2; i > 0; i--) {
for (int j = i + 1; j < dp.size(); j++) {
dp[i][j] = max(arr[i] * arr[i - 1] * arr[j + 1] + dp[i + 1][j], arr[j] * arr[i - 1] * arr[j + 1] + dp[i][j - 1]);
for (int k = i + 1; k < j; k++) {
dp[i][j] = max(dp[i][j], arr[k] * arr[i - 1] * arr[j + 1] + dp[i][k - 1] + dp[k + 1][j]);
}
}
}
return dp[1][len - 2];
}
题目四
题目理解:数组的下标表示圆盘的大小,长度是圆盘的数量,下标对应的元素是该圆盘目前所在的位置。问给定数组表示的状态,是否为最优移动轨迹出现的状态,如果是返回是第几个状态,否则返回-1。
N层汉诺塔问题的最优解要走2^N-1步:
假设现在是i层汉诺塔问题,想要将1…i从from杆移到to杆,中间可以借助other杆;三步走:1) 1…i-1的圆盘从from杆挪到other杆;2) i圆盘从from杆挪到to杆;3) 1…i-1的圆盘从other杆挪到to杆;如果i盘的状态是from,说明第一步没走完、走了多少步却决于i-1层汉诺塔问题;如果i盘的状态是to,说明当前至少走了2^i-1+1,总步数取决于3)走了多少步;如果i是other状态,说明不是最优解的任何一步。
代码实现:
//把0..i的圆盘,从from全部挪到to
//返回arr[0..i]中的状态是最优解的第几步
//N层汉诺塔问题是要分解成N-1层...1层的问题的,因此arr中出现的状态也是某个小问题的状态
//时间复杂度:O(N),递归直走一侧,且每次i都要减一
int f(vector<int>& arr, int i, int from, int other, int to) {
if (i == -1) {
return 0;
}
if (arr[i] == other) {
return -1;
}
//第一步没走完
if (arr[i] == from) {
return f(arr, i - 1, from, to, other);
}
//第三步完成的程度
int rest = f(arr, i - 1, other, to, from);
return rest == -1 ? -1 : rest + (1 << i);
}
int step(vector<int>& arr) {
if (arr.size() == 0) {
return -1;
}
return f(arr, arr.size() - 1, 1, 2, 3);
}
//改动态规划
int step2(vector<int>& arr) {
if (arr.size() == 0) {
return -1;
}
int from = 1;
int mid = 2;
int to = 3;
int i = arr.size() - 1;
int res = 0;
int temp = 0;
while (i >= 0) {
if (arr[i] == mid) {
return -1;
}
if (arr[i] == to) {
res += (1 << i);
temp = from;
from = mid;
mid = temp;
i--;
}
else {
temp = to;
to = mid;
mid = temp;
i--;
}
}
return res;
}