习题3-3(动态规划)
倒水问题
-
三种操作,倒满、倒空、把一个杯子中的水倒到另一个杯子里(直到被倒水的杯子满,或者倒水的杯子倒空)
-
上述三种操作,根据杯子的不同可以分化成6种操作
-
t<=4 搜索(暴力) O(6^t)
-
t<=100的量级用DP(有一种是类似搜索记忆法),记录两个水杯的水量,进行的操作数就可以确定当前的状态
-
dp(i)(u)(v) = 0/1 i表示操作数 u表示A杯容量 v表示B杯容量
-
怎么进行状态转移?dp(i+1)(x)(y) (a=u,b=v)->(a=x,b=y)
-
时间复杂度是O(nmt)
-
最终答案是min(|u+v-d|)
-
t<=200
-
dp(i)(u)(v) = 1 dp(i+1)(u)(v) = 1 可能存在,就有了浪费时间的地方
-
上面两式会做相同的事情
-
对i进行优化,经过几步到达(u)(v)
-
n = 3 m = 2,n是a杯容量,m是b杯容量 (0,0)(自环) <-> (3,0)(自环) -> (1,2) | ^ | V | V (0,2)(自环) (3,2) | V (2,0)
-
上图明显是一个图的操作
-
是BFS的算法,对于每一个状态,不用计算每一个操作数能否到达它,只用计算到达这个状态的最小操作数
-
复杂度是O(nm)
代码解析
-
to 倒水函数 有6种操作
-
// 倒水函数 // pii表示状态 // k表示是哪种操作 // n,m是杯子容量 pii to(pii p,int k,int n,int m){ if(k == 0)//倒空杯子一 return new pii(0,p.se); else if(k == 1)//倒空杯子二 return new pii(p.fi,0); else if(k == 2)//倒满杯子一 return new pii(n,p.se); else if(k == 3)//倒满杯子二 return new pii(p.fi,m); else if(k == 4)//将杯子二的水向杯子一倒 return new pii(min(p.fi + p.se, n), max(p.fi + p.se -n, 0)); else if(k == 5)//将杯子一的水向杯子二倒 return new pii(max(p.fi + p.se - m, 0), min(p.fi + p.se, m)); else//啥也不干 return p; }
-
int getAnswer(int n,int m,int t,int d){ // 初始化,清空队列,将mind所有位置置为-1,表示未访问 memset(mind,-1,sizeof(mind)); qh = qt = 0; q[++qt] = pii(0,0); mind[0][0] = 0; // 进行BFS while(qh < qt){ // 用数组实现的队列,qh对头,qt队尾 pii u = q[++qh];//取出队头元素 // 小小的剪枝,用u去更新,用u都走了t步,别的肯定大于t步 if(mind[u.fi][u.se] == t) break;//如果已经进行了t步,那么没必要继续搜索了,退出循环即可 for(int k = 0; k < 6; ++k){ //枚举6种策略 pii v = to(u, k, n, m); // 避免了重复计算 // 因为是bfs,一层一层的遍历,要么是同一层,要么是前面的,没有必要更新 if(mind[v.fi][v.se] != -1)//判断目标状态是否未曾到达过 continue; q[++qt] = v;//加入队列 mind[v.fi][v.se] = mind[u.fi][u.se] + 1;//记录mind } } int ans = d; for(int i = 0;i<=n;i++){ for(int j=0;j<=m;j++){ if(mind[i][j] != -1) ans = min(ans,abs(i+j-d)); } } }
奶牛吃草
- 数轴上面有n个草,每过1个单位时间,每颗草都会流失1个单位的口感,如果某棵草被奶牛吃过了,口感就不会流失了
- 奶牛站在某一个坐标位置,需要求奶牛怎么吃草使得青草流失口感最小
- 走回头路(不吃草),不走显然不是最优的操作,所以都是不考虑的操作
- 吃草顺序确定,排除上面两种操作,按最优操作吃草,吃草走过的路径也就确定了
- 暴力法:暴力枚举所有吃草顺序,模拟计算答案
标答
-
进一步推导,损失是发生在吃草的时候,更改统计方式,使得流失是在走路的时候
-
之前每走过一个单位,青草的口感减一,坏处是青草的口感跟走过的时间是相关的,而走过的时间是10^6级别,所以效率低
-
优化思路,每走一步,把所有青草流式的口感直接加到奶牛身上,所以可以通过当前还剩下的青草数目,就可以记录当前每走一步青草所流失的口感,这样记录的好处是与时间无关
-
两个性质
- 每次奶牛在做下一个决策时,最多只有两个目标,下一步走的草,一定是和它相邻的没吃的两棵草,发现这是一个区间(离它最近的左边青草,离它最近的右边的青草)
- 奶牛在一段时间内吃掉的青草,一定是被一个连续的区间包含的
-
用一个区间来描述吃掉的青草
-
需要记录的参数:被吃掉青草的集合,奶牛的位置
-
奶牛吃一颗草时,一定在区间端点上
-
dp[l][r][k=0/1]
-
dp[l][r]表示区间,同时要对草的坐标进行排序 k表示是左端点还是右端点 所以每次要么往左走要么往右走
-
复杂度O(n^2)
-
动态规划的难点在于方程和状态
代码解析
-
dp[i][i][0] = dp[i][i][1] = abs(x[i]-k) * n;
-
走第一步前,还有n棵草没被吃
-
// n是青草个数 // k是奶牛坐标 // x是描述序列,x的下标是从1开始的 int getAnswer(int n,int k,vector<int> x){ sort(x.begin()+1,x.end());// 将青草坐标排序 // 设置边界条件,只吃一棵草的情况下,答案是什么 for(int i=1;i<=n;i++) dp(i)(i)(0) = dp(i)(i)(1) = abs(x[i] - k)*n; for(int len = 1; len < n; ++len) for(int l = 1, r; (r = l + len) <= n; ++l){ // 枚举空间(先枚举区间长度,再枚举左端点,求出右端点) // 避免小的区间在大的区间之前被枚举掉 // 进行转移 // 往左走 // 又分两种情况,之前就停在旧的左端点,和旧的右端点 // 从l+1走到l,或者从r走到l dp[l][r][0]= min(dp[l + 1][r][0] + (n - r + 1)* abs(x[l] - x[l + 1]), dp[l + 1][r][1] + (n - r + 1)* abs(x[l] - x[r] )); // 往右走 // 从r-1走向r,或者从l走向r dp[l][r][1]= min(dp[l][r - 1][1]+(n - r + 1)* abs(x[r] - x[r - 1]), dp[l][r - 1][0]+(n - r + 1)* abs(x[r] - x[l])); } return }
-
是从dp[l+1][r][0] 走到dp[l][r][0]即从左端点向左走了一步,dp[l][r-1][1]走到dp[l][r][1]即从右端点向右走了一步
-
r-l表示已经吃掉的青草数目,不能简单的去分别枚举r和l
最长公共子序列
- O(n2)的动态规划算法
经典算法
-
dp(i)(j)分别表示a序列的前i位、b序列的前j位
- 考虑状态转移,dp(i)(j)可以由dp(i)(j-1)或dp(i-1)(j)得到
- 当a[i]==a[j]时,dp(i-1)(j-1)+1
-
O(n2),状态的复杂度是O(n2)的,转移复杂度是O(1)的
-
(1) dp[0][j] = dp[i][0] = 0 (任意的i,j) (2) for i for j (3) (4) dp[n][n]
最长上升子序列(LCS)
- 怎么从求最长上升子序列的问题转换成最长公共子序列的问题
- a,b两个序列都是排列,把b中所有元素替换成a中该元素出现的位置,进行这样的替换,得到一个新的序列,求这个序列的上升序列(LIS)
时间复杂度为O(nlogn)的解法(求解最长上升子序列)
- f(i) 长度为i的所有最长上升子序列的最小结尾是多少
- 每插入一个数x到末尾,都需要对f(i)进行更新,每次都只会对一个f(i)进行更新,这个i的位置也就是f(i-1)小于x的最大i,也就是末尾比x小的尽可能长的序列
- 利用二分查找进行插入
代码解析
-
把b中每个元素出现的位置替换到a序列中,得到一个新序列
-
vector<int> pos,f; // 计算最长公共子序列的长度 // n:表示两序列长度 // a:描述序列a // b:描述序列b int LCS(int n,vector<int> a,vector<int> b){ // 初始化,调整pos,f数组的长度,并将f数组置初值 pos.resize(n+1); f.resize(n+2,inf); // 记录b序列中各元素出现的位置 for(int i=1;i<=n;i++) pos[b[i]] = i; // 处理a序列 for(int i=1;i<=n;i++) a[i] = pos[a[i]]; // 将f[0]初值置为0 f[0] = 0; // 二分需要修改的f位置并进行修改 // 填入lower_bound和upper_bound中的一个 for(int i=1;i<=n;i++) *lower_bound(f.begin(),f.end(),a[i]) = a[i]; // 使f[i]!=inf的最大i return int(lower_bound(f.begin(),f.end(),n+1)-f.begin()-1; }
-
f(1)中存储了长度为1的上升子序列的最小结尾下标,f(2)中存储了长度为2的上升子序列的最小结尾下标
-
最后的答案lower_bound是找到比相应元素大于等于的第一个元素下标,而f中没有最长子序列的位置都是1e9的,所以找到的下标减1就表示了最长的上升子序列
拓展
- 如果不是排列,是任意序列,所有数的出现次数不会太大(5到10),重数出现的次数非常少,怎么解决?