论文阅读——《动态规划》 作者 方奇
关健字:阶段 状态 决策 函数递推式
动态规划:指一种解决多阶段决策最优化问题的方法,动态指在每一个可能出现的情况中做出决策后引起状态转移。
例题1:现有一张地图,各结点代表城市,两结点间连线代表道路,线上数字表示城市间的距离。如图1所示,试找出从结点1到结点10的最短路径。
- 本题可以直接使用穷举法,将节点1——10的所有路线都走一边,记录一个最小值
- 仔细看一波图,发现可以将图分为五层:1,23,456,789,10(每层的点不相连,除了第一层和第五层,其他层都可以且只能到达上和下层)
- 对分层进行分析的话,可以看出中间三层每一层上每个节点都可以作为上一层的重点和这一层的起点。
- 这时候问题就变成了:求出由第一层到第五层的最小距离。又因为每一层都是相对独立的,到每一层的某个节点的决策,只依赖于上一层的计算结果,这样就成功的将问题分为了多个阶段
- 假设目前处于第三层,那么第一层到4的距离是5,到5的距离是6,到6的距离是5,下面要求到第4层8的距离min(6+5,5+5)=10
- 这样就大大减少了计算量。
动态规划中的一些术语:
- 阶段:将问题分为几个相互联系的有顺序的几个环节,这些环节称作阶段。
- 状态:某个阶段的起始位置。(上面图1中的点2,3就是第二个阶段的状态)
- 决策:从某阶段的某个状态演变到下一个阶段的某个状态的选择
- 策略:从开始到结束的每段决策组成的序列称为策略
- 状态转移方程:决策的运行公式,可使得前一阶段的某状态照此变为下一阶段的某个状态。
- 目标函数与最优化概念:目标函数是衡量多阶段决策过程优劣的准则。最优化概念是在一定条件下找到一个途径,经过按题目具体性质所确定的运算以后,使全过程的总效益达到最优。
动态规划的使用前提:
- 满足最优化原理:即全局最优可以由每一个状态下的最优推导得出。
- 无后效性: 前面做出的决策不会影响后面决策,过去的步骤只能通过现在的状态影响未来发展。当前决策与后续决策无关。状态出现在策略的任何一个位置都可实施相同策略。
动态规划的计算方法:
- 分析题意,判断是否满足上述的两个条件
- 尝试确定状态,阶段,约束和边界条件
- 推导状态转移方程
例题2:
排队买票
问题描述:一场演唱会即将举行。现有N(O〈N〈=200)个歌迷排队买票,一个人买一张,而售票处规定,一个人每次最多只能买两张票。假设第I位歌迷买一张票需要时间Ti(1〈=I〈=n),队伍中相邻的两位歌迷(第j个人和第j+1个人)也可以由其中一个人买两张票,而另一位就可以不用排队了,则这两位歌迷买两张票的时间变为Rj,假如Rj〈Tj+Tj+1,则这样做就可以缩短后面歌迷等待的时间,加快整个售票的进程。现给出N,Tj和Rj,求使每个人都买到票的最短时间和方法。
先判断是否满足上述条件:当所有人买票最快时,显然n-1个人买票一定也最快,n-2个人也最快。并且前面的人怎么买票与后面的人怎么买票无关
那么阶段和状态也很好划分,阶段就是买票的先后顺序,状态就是第i个人买票。,那么记s[i]为第i个人买完票之后所花的最短时间
则这个人可以自己买票,也可以和自己前面的人一起买,s[i]=min(s[i-1]+Ti,s[i-2],Ri-1);
这样从前往后进行遍历就可以求出时间,初始化s[1]=T1;
当然也可以倒推 记dp[i]为倒着买票到第i个人,则有dp[i]=min(dp[i-1]+Ti,dp[i-2]+R(i+1));自己买或者和前面的人一起买
那么初始状态dp[n]=Tn,dp[n+1]=0,其余全为998244353(INF)
例题3:
给你一张图,求A到J的最短路
你看这张图,它又长又宽!
- 刚才的套路行不通了,这张图没有办法分出层次(点EGIJH了解一下)
- 但是这肯定还是能DP的,只不过状态要改一下。
- 所以我们换个角度考虑,每次都以某个状态为起点,遍历由它引申出去的路径,等所有已知状态都扩展完了,再来比较所有新状态,把值最小的那个确定下来,其他的不动。
- 如此,我们就得到了Floyd算法:dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]) dp[i][j]表示i和j两点的最短距离,k为一个中继点
例题4:
复制书稿(BOOKS)
问题描述:假设有M本书(编号为1,2,…M),想将每本复制一份,M本书的页数可能不同(分别是P1,P2,…PM)。任务时将这M本书分给K个抄写员(K〈=M〉,每本书只能分配给一个抄写员进行复制,而每个抄写员所分配到的书必须是连续顺序的。
意思是说,存在一个连续升序数列0=bo〈b1〈b2〈…<bk-1 <bk=m,这样,第I号抄写员得到的书稿是从bi-1+1到第bi本书。复制工作是同时开始进行的,并且每个抄写员复制的速度都是一样的。所以,复制完所有书稿所需时间取决于分配得到最多工作的那个抄写员的复制时间。试找一个最优分配方案,使分配给每一个抄写员的页数的最大值尽可能小(如存在多个最优方案,只输出其中一种)。
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 using namespace std; 5 int dp[501][501]; 6 int rt[501]; 7 int w[501]; 8 int s[501]; 9 int m,k; 10 void print(int l,int r) 11 { 12 int ss=0; 13 for(int i=r;i>=l;i--) 14 { 15 if(ss+w[i]>dp[m][k]) 16 { 17 print(l,i); 18 cout<<i+1<<' '<<r<<endl; 19 return; 20 } 21 ss+=w[i]; 22 } 23 cout<<1<<' '<<r<<endl; 24 } 25 int main() 26 { 27 scanf("%d%d",&m,&k); 28 memset(dp,0x7f7f7f7f,sizeof(dp)); 29 for(int i=1;i<=m;i++) 30 { 31 scanf("%d",&w[i]); 32 s[i]=s[i-1]+w[i]; 33 dp[i][1]=s[i]; 34 } 35 if(m==0&&k==0) 36 return 0; 37 for(int j=2;j<=k;j++) 38 { 39 for(int i=j;i<=m;i++) 40 { 41 for(int sk=j;sk<=i;sk++) 42 { 43 dp[i][j]=min(max(dp[sk-1][j-1],s[i]-s[sk-1]),dp[i][j]); 44 } 45 } 46 } 47 print(1,m); 48 return 0; 49 }
记dp[i][j]为第i本书由第j位抄写员抄写时最大页数的最小值,那么显然可知,我们需要枚举一个书本分割点,代表分割点以后的书都是由第j位抄写员抄写的
假定分割点为v,分割点这本书由抄写员j来抄写,那么就有dp[i][j]=min(dp[i][j],max(dp[v-1][j-1],sum[i]-sum[v-1]);sum为书本页数的前缀和
因为求的是最小值,所以原来的dp[i][j]和用来更新的状态取最小,又因为dp[v-1][j-1]是前j-1个抄写员抄的最大值,sum[i]-sum[v-1]是第j位抄写员写的,因此要求max
初始状态:dp[i][1]=sum[i],其余均为998244353(INF)
取值范围:因为每个抄写员都至少一本书(洛谷),那么枚举过程中最外层枚举J,内层枚举I,J<=I<=M(既保证每个人都有本书抄,又保证不超过M本)
最内层枚举分割点V,可知J<=V<=I(分割点肯定是保证每个人有书抄,但是不超过当前枚举的书数)
这样的话求最小值的任务就完成了,下面是输出方案(洛谷上的要求前面的抄写员工作量尽可能的小)
那么已经有最大值了,就从最后一个往前贪呗,尽可能的让后面的员工在不超过上限的情况下尽量多抄。
例题5:
多米诺骨牌(DOMINO)
问题描述:有一种多米诺骨牌是平面的,其正面被分成上下两部分,每一部分的表面或者为空,或者被标上1至6个点。现有一行排列在桌面上:
顶行骨牌的点数之和为6+1+1+1=9;底行骨牌点数之和为1+5+3+2=11。顶行和底行的差值是2。这个差值是两行点数之和的差的绝对值。每个多米诺骨牌都可以上下倒置转换,即上部变为下部,下部变为上部。
现在的任务是,以最少的翻转次数,使得顶行和底行之间的差值最小。对于上面这个例子,我们只需翻转最后一个骨牌,就可以使得顶行和底行的差值为0,所以例子的答案为1。
这个题洛谷上也有,这里给出一种比论文解法更优的做法(虽然也不见得优多少)
众所周知,将骨牌反转的话骨牌上下两行的值之差变化为2*(abs(u[i]-d[i]))u==up,d==down
那么是否可以考虑将一张骨牌上的两个值转换为它们的差s[i]=(u[i]-d[i]),再进行计算呢?
可以,因为骨牌上的两个值是不变的,反转也只会产生上述变化,因此可以直接使用一个值的加减代替这两个参数
问题是怎样计算差值?观察数据可知,数据波动不会超过+-5000(一共1000对数,最大值为6最小值为1,差值-5<=x<=5);
那么就开个大点的数组,加个偏移量mid,比如给mid定义为6005,开一个大小为13000的数组一定没问题(这点内存不会卡,开大点不费电)
那么记dp[x][i]为翻到第x张骨牌下面减去上面差值为i的情况下的最小反转次数,则一定有dp[x][i]=min(dp[x][i],dp[x-1][i+s[x]])和dp[x][i]=min(dp[x][i],dp[x-1][i-s[x]]+1)
第一个方程的意思是由前x-1张骨牌下面减去上面差值为i+s[x]的状态转变到i也就是相当于这枚骨牌不反转(相当于上面加上了s[x])
第二个方程自然就是反转,给下面加上s[x]
求出这些之后直接扫秒一边-5000到5000的dp[n],找一个差值最接近0的反转次数最小值
1 // luogu-judger-enable-o2 2 #include<iostream> 3 #include<cstdio> 4 #include<cstring> 5 using namespace std; 6 int dp[1001][13011];//开大点不要钱 7 int as[1001]; 8 int n,mid=6020;//开大点不费电 9 int abs(int x) 10 { 11 return max(x,-x); 12 } 13 int main() 14 { 15 scanf("%d",&n); 16 int a,b; 17 for(int i=1;i<=n;i++) 18 { 19 scanf("%d%d",&a,&b); 20 as[i]=a-b; 21 } 22 memset(dp,0x7f7f7f7f,sizeof(dp)); 23 dp[0][mid]=0; 24 //数据上下波动不超过6-1=5; 25 for(int i=1;i<=n;i++) 26 { 27 for(int j=1020;j<=11020;j++) 28 { 29 dp[i][j]=min(dp[i][j],dp[i-1][j+as[i]]);//这张本来就是正的 30 dp[i][j]=min(dp[i][j],dp[i-1][j-as[i]]+1);//这张给他翻过来 31 } 32 } 33 int minn=998244353,ans=998244353; 34 for(int j=1020;j<=11020;j++) 35 { 36 if(dp[n][j]>=0x7f7f7f7f) 37 { 38 continue; 39 } 40 if(abs(j-mid)<=minn) 41 { 42 if(abs(j-mid)==minn) 43 ans=min(ans,dp[n][j]); 44 else 45 ans=dp[n][j]; 46 minn=abs(j-mid); 47 } 48 } 49 cout<<ans; 50 return 0; 51 }
完结撒花!