习题3-3(动态规划)

习题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),重数出现的次数非常少,怎么解决?

猜你喜欢

转载自blog.csdn.net/Markland_l/article/details/109270341
3-3