2017清北学堂集训笔记——动态规划Part2

啊~到下午啦,我们进入Part2!——一个简洁的开头

我们来探讨第一类问题——路径行走问题

经典例题:方格取数(Luogu 1004)

设有 N*N 的方格图 (N<=9),我们将其中的某些方格中填入正整数,而其他的方格中则放入数字 0。
* 某人从图的左上角的 A 点出发,可以向下行走,也可以向右走,直到到达右下角的 B 点。在走过的路上,他可以取走方格中的数(取走后的方格中将变为数字 0)。
* 此人从 A 点到 B 点共走两次,试找出 2 条这样的路径,使得取得的数之和为最大。
- 与数字金字塔很类似?如果只走一次呢?

* 只走一次:(仿照数字金字塔)记录 F[i][j] 为走到第 i 行第 j 列的最大值。
- 思考:转移的顺序?转移的方程?

* 问题:在这道题目当中我们不能直接套用走一次的方法;一个方格只能被取走一次(也就是说每个权值只能被取用一次)。 
- 考虑两条道路同时进行:状态 F[i][j][k][l] 来记录第一条路径走到(i,j),而第二条路径走到 (k,l) 的最大值。 

* 转移方程:考虑逆推(我可能是由哪些状态得到的)。

在这里,我们要保证这两个点所走的步数是相同的,那么这个状态才是有意义的,在这里没有这样算,不算也是对的,算了也不影响答案。。
- 每个点可以往下走或者往右走;一共走到有 2*2=4 种可能性(时刻注意边界情况)

 1 //T11:方格取数(DP/逆推)
 2 for(int i=1;i<=n;++i)
 3 for(int j=1;j<=n;++j)
 4 for(int k=1;k<=n;++k)
 5 for(int l=1;l<=n;++l)
 6 {
 7     //注意,如果走到了一起,只加一次
 8     int cost=a[i][j]+a[k][l]-a[i][j]*(i==k&&j==l);//如果两个位置是重叠的,就要减去重复的
 9     //四种可能性;考虑:为什么不加边界情况的判断?
10     f[i][j][k][l]=max(max(f[i-1][j][k-1][l],f[i-1][j][k][l-1]),max(f[i][j-1][k-1][l],f[i][j-1][k][l-1]))+cost;
11 }

最长不下降子序列问题:

经典例题:导弹拦截(Luogu 1020)

某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。
* 输入导弹依次飞来的高度(雷达给出的高度数据是不大于 30000 的正整数),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。 
-389 207 155 300 299 170 158 65

-最多能够拦截: 6;最少要配备: 2

* LIS:一个序列当中一段不下降的子序列。
* 这道题目中第一问要求我们找到一段最长的单调下降的子序列。 (无论是上升还是下降,可以使用类似的算法解决)
* 状态:我们用 F[i] 代表,以i 位置为结尾的一段, 最长的下降子序列。
! 最优性:如果某段 [q1q2q3 ...qn] 是以qn结尾的最长下降子序列;那么去掉最后一个的序列 [q1q2q3...qn-1],依然是以qn-1结尾的最长下降子序列。(一个这是一个全局最优,每一步的最优解构成全局最优,所以拆成部分还是最优解,满足无后效性原则)

* 逆推:假设我们需要求以 X 结尾的最长下降子序列 F[X];
* 由最优性可得,我们除去最后一个位置(也就是 X),还是一段最长下降子序列。
* 那我们可以枚举这个子序列的结尾 Y,最优值就是 F[Y]。
! 但需要注意的是,必须保证A[X] < A[Y], X 比 Y 要低,才满足下降的要求。
* 我们从所有枚举的结果中找到一个最大的即可

例如:我们看到这坨绿油油的图,我们讨论F[8]作为谁的结尾,由图中我们可以得到:

F[8]=F[4]+1(F[8]可以作为F[4]的结尾);  F[8]=F[6]+1(F[8]同样可以作为F[6]的结尾);  F[8]=F[7]+1(F[8]可以作为F[7]的结尾)

但是最长的还是F[8]作为F[4]结尾,这时候最长,取max

* 注意到题目还需要计算‘如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。’
* 可以直接观察得到,所求的答案至少为原题的最长不下降子序列。
- 因为它们当中,任意两个都不可能被同一个导弹打中。
- 事实可以证明,这就是答案。—— 啊喂?不给个证明嘛→_→?那肯定给啊!证明如下!

证明:因为假设一个导弹a被打中了,那么下次有比它高的导弹b,就没办法用打中a的导弹系统来打b,必须增加一个导弹系统,所以为最长不下降子序列长度。

同样地,我们还可以运用极限思维来考虑,有n颗导弹,它们的高度都是单调递增的,那么这时就必须要n个系统来拦截,所以为最长不下降子序列长度=n;

 1 //T12:导弹拦截(DP/LIS/逆推)
 2 int ansf=0,ansg=0;//记录所有的f(g)中的最优值
 3 //f计算下降子序列,g计算不下降子序列
 4 for(int i=1;i<=n;++i)
 5 {////枚举倒数第二个,寻找最长下降放到f中,最长不下降放到g中
 6     for(int j=1;j<i;++j)
 7     if(a[j]>a[i]) f[i]=max(f[i],f[j]);
 8     else g[i]=max(g[i],g[j]);
 9     ++f[i],++g[i];//加上自己的一个
10     ansf=max(ansf,f[i]);
11     ansg=max(ansg,g[i]);
12 }
13 cout<<ansf<<endl<<ansg<<endl;//输出答案

问题变形:

经典例题:合唱队形(Luogu 1091)

*N位同学站成一排,音乐老师要请其中的(N-K)位同学出列,使得剩下的 K 位同学排成合唱队形。
*合唱队形是指这样的一种队形:设 K 位同学从左到右依次编号为1,2,…,K,他们的身高分别为T1,T2,...,TK,则他们的身高满足T1<...<Ti>Ti+1>...>TK(1≤i≤ K)。
* 你的任务是,已知所有 N 位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。
- 186 186 150 200 160 130 197 220
- 最少需要 4 位同学出列 
所谓合唱队形,就是要一个队列高度构成一个山峰的形状,有顶尖,两边单调递减。

我们可以采用枚举每个人为顶尖,从那个定点开始,向左、右寻找单调递减序列的最大和,这样。。。真的很麻烦。

* 问题转化:最少的同学出列 -> 尽量多的同学留在队列
* 与 LIS 的联系:如果确定了中间的“顶尖” ,两侧就是“单调上升” 和“单调下降” 的。 
           

* 状态设计: F[i] 与 G[i](预先处理)
- F[i]:以 i 为端点,左侧的最长的上升子序列长度。
- G[i]:以 i 为端点,右侧的最长的下降子序列长度。
这个思路就是要我们找到每个以第i个点为末端的最长上升子序列和最长下降子序列,最终我们枚举每一个点i访问f[i]、g[i],找出两者相加最大值即可。

同样,我们还有另一种思路,找到了最长上升子序列和最长下降子序列,两个序列合并,去掉中间重复的元素(出现顶尖),即为答案。(这里我就不粘代码了,详见我之前写这个题的博客吧→_→)

 1 //T13:合唱队形(DP/LIS/逆推)
 2 for(int i=1;i<=n;++i) cin>>a[i],f[i]=g[i]=1;
 3 for(int i=1;i<=n;++i)
 4 for(int j=1;j<i;++j)
 5     if(a[i]>a[j]) f[i]=max(f[i],f[j]+1);
 6 for(int i=n;i;--i)//g的计算从反方向进行枚举
 7     for(int j=n;j>i;--j)
 8         if(a[i]>a[j]) g[i]=max(g[i],g[j]+1);
 9 int ans=0;
10 /*把最长上升和最长下降的一部分部分拼在一起求总长度最大*/
11 for(int i=1;i<=n;++i)//枚举每一个顶点为顶尖
12 {
13     ans=max(ans,f[i]+g[i]-1);//"-1"表示减掉顶尖这个重复计算的点
14 }

最长公共子序列:

经典例题:排列LCS问题(Luogu 1439)

* 给出 1-n 的两个排列 P1 和 P2,求它们的最长公共子序列。
* 公共子序列:既是 P1 的子序列,也是 P2 的子序列。
- 3 2 1 4 5
- 1 2 3 4 5
- 最长公共子序列 (LCS): 3([1 4 5])

* LCS:两个序列的最长公共子序列。
* 状态:我们用 F[i][j] 代表,前一个序列以 i 位置为结尾,后一个序列以 j 位置为结尾,它们的最长公共子序列。
! 最优性:如果某段 [q1q2q3...qn] 是分别以i,j结尾的最长公共子序列;那么去掉最后一个的序列 [q1q2q3...qn-1],依然是以 i - 1; j - 1结尾的最长公共子序列。

* 逆推:假设我们需要求两个序列分别以 i,j 结尾的最长公共子序列F[i][j],接下来我们可以分几种情况讨论:
- A[i] 不在公共子序列中,那么长度则等于 F[i-1][j];
- B[j] 不在公共子序列中,那么长度则等于 F[i][j-1];
- A[i] 与 B[j] 都在子序列中,并且两者匹配,那么长度等于F[i-1][j-1]+1;
* 我们从所有枚举的结果中找到一个最大的即可。

* 逆推:假设我们需要求两个序列分别以 i,j 结尾的最长公共子序列F[i][j],可能的三种情况:

1 //T16:排列LCS问题(DP/LCS/50分数据规模限制)
2 for(int i=1;i<=n;++i)
3     for(int j=1;j<=n;++j)
4     {
5         //分三种情况进行讨论
6         f[i][j]=max(f[i-1][j],f[i][j-1]);//如果两个相同,娶一个最大值
7         if(p[i]==q[j]) f[i][j]=max(f[i][j],f[i-1][j-1]+1);
8     }
9 int ans=f[n][n];

仅仅在这个问题中,LCS是可以转化为LIS的:

* 假定某一个序列为 [1 2 3 ... N],那么答案则是另一个序列的 LIS;(因为满足严格的单调性质)
- 3 2 1 4 5
- 1 2 3 4 5
* 但如果两个序列都不是 [1 2 3 ... N] 呢?通过转化使一个序列变成它(过程为:我们把第一个序列的第一个数5变成1,第二个序列的1变成5;把第一个序列的第二个数3变为2,第二个序列的2变为3,以此类推...),而答案不变。
- 5 3 4 1 2 -> 1 2 3 4 5
- 3 5 1 2 4 -> 2 1 4 5 3

这种转换只能用在所给序列有不重复元素,长度相同,如果不满足,就会出现重复,会出错。

问题变形:

经典例题:字串距离(Luogu 1279)

* 设有字符串 X,我们称在 X 的头尾及中间插入任意多个空格后构成的新字符串为 X 的扩展串,如字符串 X 为” abcbcd”,则字符串“abcb□cd”,“□a□bcbcd□”和“abcb□cd□”都是 X 的扩展串,这里“□”代表空格字符。
* 如果 A1 是字符串 A 的扩展串, B1 是字符串 B 的扩展串, A1 与 B1具有相同的长度,那么我扪定义字符串 A1 与 B1 的距离为相应位置上的字符的距离总和,而两个非空格字符的距离定义为它们的 ASCII 码的的字符的距离总和,而两个非空格字符的距离定义为它们的 ASCII 码的差的绝对值,而空格字符与其他任意字符之间的距离为已知的定值 K,空格字符与空格字符的距离为 0。在字符串 A、 B 的所有扩展串中,必定存在两个等长的扩展串 A1、 B1,使得 A1 与 B1 之间的距离达到最小,我们将这一距离定义为字符串 A、 B 的距离。
* 请你写一个程序,求出字符串 A、 B 的距离。

- cmc
- snmn
- 2
- 距离为:10

* 状态设计:仿照最长公共子序列,我们设计状态 F[i][j] 为前一个序列以 i 结尾,后一个序列以 j 结尾的最小距离;同样也有以下三种情况
- A[i] 与空格匹配:距离为 F[i-1][j] + K; (K 为到空格的距离)
- B[j] 与空格匹配:距离为 F[i][j-1] + K;
- A[i] 与 B[j] 匹配:距离为 F[i-1][j-1] + |A[i] - B[j]|;(ASCII 码的差的绝对值)

 1 //T17:字串距离(DP/LCS)
 2 //初值:在遇到最小值的问题,一定要小心初值的处理
 3 f[0][0]=0;
 4 for(int i=1;i<=n;++i) f[i][0]=i*k;
 5 for(int j=1;j<=m;++j) f[0][j]=j*k;
 6 for(int i=1;i<=n;++i)//n为第一个串的长度
 7 for(int j=1;j<=m;++j)//m为第二个串的长度
 8 {
 9     //三种情况
10     f[i][j]=min(f[i-1][j],f[i][j-1])+k;
11     f[i][j]=min(f[i][j],f[i-1][j-1]+abs(p[i-1]-q[j-1]));
12 }
13 int ans=f[n][m];

二维平面问题:

 经典例题:家的范围(Luogu 2733)

  农民约翰在一片边长是 N (2 <= N <= 250) 英里的正方形牧场上放牧他的奶牛。 (因为一些原因,他的奶牛只在正方形的牧场上吃草。 ) 遗憾的是, 他的奶牛已经毁坏一些土地。 ( 一些 1 平方英里的正方形)
  农民约翰需要统计那些可以放牧奶牛的正方形牧场 (至少是 2x2 的, 在这些较大的正方形中没有一个点是被破坏的,也就是说,所有的点都是“ 1” )。
  你的工作要在被供应的数据组里面统计所有不同的正方形放牧区域(>=2x2) 的个数。当然,放牧区域可能是重叠。
如右图所示:以2、3、4为边长的正方形共有10、4、1个,不妨转换一下思路:我们只考虑以(x,y)这个点能构成的最大正方形是到多大?这样把问题化简就行了。

*状态设计:设状态 F[i][j] 为以 (i,j) 位置为右下角,最大的一个正方形区域。

* 考虑位置 F[i-1][j] 与 F[i][j-1];
- 则有 F[i][j] <= F[i-1][j] + 1; F[i][j] <= F[i][j-1] + 1;

上图中,*号位置(i,j)的大小由上方和左方决定,*号位置的F[i][j]=4而≠5,因为*号的值不能超过4也不能超过5,所以只能取4,要取F[i-1][j] + 1、F[i][j-1] + 1两者中较小的一个。

很明显, F[i][j-1] 与 F[i-1][j] 中较小值限制了最大区域的边长。

* 但在某些情况下,仅考虑 F[i][j-1] 与 F[i-1][j] 显然不全面,还需要考虑 F[i-1][j-1]。

* 所以我们可以得到最终的表达式: F[i][j] = MIN(F[i-1][j],F[i][j-1], F[i-1][j-1]) + 1 (当 (i,j) 不为障碍时)

 1 //T18:家的范围(DP)
 2 //边界初值
 3 for(int i=0;i<n;++i) f[i][0]=(a[i][0]==‘1‘);//t[i]表示最大边长为i的正方形的个数
 4     for(int j=0;j<n;++j) f[0][j]=(a[0][j]==‘1‘);
 5         for(int i=1;i<n;++i)
 6             for(int j=1;j<n;++j)
 7                 if(a[i][j]==‘1‘)//如果是非障碍
 8                 {//计算
 9                     f[i][j]=min(min(f[i-1][j],f[i][j-1]),f[i-1][j-1])+1;
10                     t[f[i][j]]++;//当前这个规格为i的正方形的个数++
11                 }
12 for(int i=n;i;i--)/*统计所有方形的数目——(t[i]已经算出来了以(i,j)这个点为右下角能够成的最大正方形
13                   总数,但是题目中要求总和,所以这个for就是求以(i,j)为右下角的正方形总数),
14                   例如:以(i,j)为右下角的4*4正方形可以提供一个以(i,j)为右下角的3*3正方形,
15                   一个以(i,j)为右下角的2*2正方形…以此类推,倒着累加即可*/
16     t[i-1]+=t[i];
17 for(int i=2;i<=n;++i)
18     if(t[i])//输出结果
19         cout<<i<<" "<<t[i]<<endl;

区间动态规划:

经典例题:游戏 A Game(Luogu 2734)

  有如下一个双人游戏:N(2 <= N <= 100) 个正整数的序列放在一个游戏平台上,游戏由玩家 1 开始,两人轮流从序列的任意一端取一个数,取数后该数字被去掉并累加到本玩家的得分中,当数取尽时,游戏结束。以最终得分多者为胜。
  编一个执行最优策略的程序,最优策略就是使玩家在与最好的对手对弈时,能得到的在当前情况下最大的可能的总分的策略。你的程序要始终为第二位玩家执行最优策略。

* 状态设计方法为: F[i][j] 表示假若只用第 i 个数与第 j 个数之间的数进行游戏, 先手能获得的最高的分为多少。
* 我们可以根据先手取左边还是取右边来决定哪种情况得分高。
! 当先手取完一件后, 先后手发生交换,而问题是类似的,如下图:

在左图中,我们已经计算出了7、2、9、5、2中先手能获得的最大值为14,后手为11,当先手取第一个数4时,先手=后手+4,后手=先手(发生了交换)。

同样地,在右图中,我们已经计算出了4、7、2、9、5中先手能获得的最大值为11,后手为16,当先手取最后一个数2时,先手=后手+2,后手=先手(同样发生了当前max值得交换)。

- 考虑: 如何决定状态转移顺序?先算哪些,后算哪些?如何书写状态转移方程?
在例子中,我们要计算长度为6的区间内的最优值,我们必须要算出左边长度为5的、右边长度为5的两个区间的最优值,所以可以先算长度为1的区间,再算长度为2的区间,再算长度为3的区间…就可以得到最大区间最优值。

 1 //T19:游戏
 2 cin>>n;
 3 for(int i=1;i<=n;++i)
 4     cin>>a[i],
 5 s[i]=s[i-1]+a[i];//s代表前缀和
 6 for(int i=1;i<=n;++i) f[i][i]=a[i];//初值(先手最后只剩下一个数,就是我自己)
 7 for(int k=2;k<=n;++k)//按照序列的长度进行枚举(k为区间长度)
 8     for(int i=1,j;i+k-1<=n;++i)
 9     {
10         j=i+k-1;
11         f[i][j]=max(s[j]-s[i]-f[i+1][j]+a[i],s[j-1]-s[i-1]-f[i][j-1]+a[j]);//两者取较大值=max(先取最左边先手最大得分,先取最右边先手最大得分)
12     }             //左边总和-先手玩家得分+自己当前取得的分数=总得分
13 cout<<f[1][n]<<" "<<s[n]-f[1][n]<<endl;//输出答案

经典例题:加分二叉树(Luogu 1040)

  设一个 n 个节点的二叉树 tree 的中序遍历为( 1,2,3,…,n),其中数字 1,2,3,…,n 为节点编号。每个节点都有一个分数(均为正整数),记第 i 个节点的分数为 di, tree 及它的每个子树都有一个加分,任一棵子树 subtree(也包含 tree 本身)的加分计算方法如下:
  subtree 的左子树的加分 × subtree 的右子树的加分+ subtree 的根的分数。
  若某个子树为空,规定其加分为 1,叶子的加分就是叶节点本身的分数,不考虑它的空子树。
  试求一棵符合中序遍历为( 1,2,3,…,n)且加分最高的二叉树 tree。要求输出;
1.tree 的最高加分:
2.tree 的前序遍历:
- 5 7 1 2 10
- 答案 1: 145
- 答案 2: 3 1 2 4 5
设 F[i][j] 为只用第 i 个数到第 j 个数构成的加分树的最大权值。下图为样例解释:

牢记一个二叉树的性质:中序遍历时候,左右子树一定在根节点左右两边

* 枚举根节点,这样就化成了左子树和右子树的问题,求最优解即可。
* F[i][j] = MAX ( F[i][k-1] * F[k+1][j] + A[k] )(左×右+根k自己本身权值)

 1 //T25:加分二叉树
 2 for(int i=1;i<=n;++i) f[i][i]=a[i];//赋初值(只有一个叶子节点,根就是自己)
 3 for(int i=0;i<=n;++i) f[i+1][i]=1;
 4 for(int k=1;k<n;++k)
 5     for(int i=1;i+k<=n;++i)
 6     {
 7         int j=i+k;
 8         for(int l=i;l<=j;++l) f[i][j]=max(f[i][j],f[i][l-1]*f[l+1][j]+a[l]);//枚举根节点
 9     }
10 int ans=f[1][n];

* 问题: 如何求出树的前序遍历(树的形态)

我们另外记录一个辅助数组 G[i][j],代表 F[i][j] 取最大值的时候,根节点是什么,这样就可以通过递归来求出树的前序遍历。

 1 for(int i=1;i<=n;++i) f[i][i]=a[i],g[i][i]=i;//边界值(只有一个叶子节点,根就是自己)
 2 for(int i=0;i<=n;++i) f[i+1][i]=1;//预处理空节点,保证不出错,一个根节点没有左子树,把左子树标记为1
 3 for(int k=1;k<n;++k)//k:区间长度
 4 for(int i=1;i+k<=n;++i)
 5 {
 6     int j=i+k;//j:末尾节点
 7     for(int l=i;l<=j;++l)
 8     {
 9         long long t=f[i][l-1]*f[l+1][j]+a[l];
10         if(t>f[i][j])//记录最优的根
11         {
12             f[i][j]=t;
13             g[i][j]=l;
14         }
15     }
16 }
 1 //T25:加分二叉树
 2 //递归输出x到y这个树的前缀遍历
 3 void dfs(int x,int y)
 4 {
 5     if(x>y) return;
 6     int l=g[x][y];//l为根
 7     cout<<l<<" ";//先输出l
 8     /*=====================*///再输出子树的值
 9     dfs(x,l-1);//左
10     dfs(l+1,y);//右
11     /*=====================*/
12 }
13 ...
14 //输出答案——整棵树
15 dfs(1,n);

过程型状态划分:

经典例题:传球游戏(Luogu 1057)

  上体育课的时候,小蛮的老师经常带着同学们一起做游戏。这次,老师带着同学们一起做传球游戏。
  游戏规则是这样的: n 个同学站成一个圆圈,其中的一个同学手里拿着一个球,当老师吹哨子时开始传球,每个同学可以把球传给自己左右的两个同学中的一个(左右任意),当老师在此吹哨子时,传球停止,此时,拿着球没有传出去的那个同学就是败者,要给大家表演一个节目。
  聪明的小蛮提出一个有趣的问题:有多少种不同的传球方法可以使得从小蛮手里开始传的球,传了 m 次以后,又回到小蛮手里。两种传球方法被视作不同的方法,当且仅当这两种方法中,接到球的同学按接球顺序组成的序列是不同的。比如有三个同学 1 号、 2 号、 3 号,并假设小蛮为 1号,球传了 3 次回到小蛮手里的方式有 1->2->3->1 和 1->3->2->1,共 2 种。

* 分析:这道题目十分容易用搜索解决。 为什么?
* 因为题目已经明确给定了过程:传球的次数。
* 因为这个过程是一定按照顺序进行的,所以可以直接写出状态:
* 设状态 F[i][j] 为传到第 i 次,现在在第 j 个人手上的方案数。
* 很显然 F[i] 只和 F[i-1] 有关;因为题目已经规定好了传递顺序。
! 这一类的过程型问题只需要找出事情发展的顺序,就可以很简单的写出状态与转移方程。

 1 //T26:传球游戏
 2 f[0][1]=1;//赋初值
 3 for(int i=1;i<=m;++i)
 4 {
 5     f[i][1]=f[i-1][2]+f[i-1][n];//头与尾需要特殊处理(第1号从2号和n号传过来)
 6     f[i][n]=f[i-1][n-1]+f[i-1][1];
 7     for(int j=2;j<n;++j)//转移只和i-1有关系(因为我已经规定好了发展顺序)
 8         f[i][j]=f[i-1][j-1]+f[i-1][j+1];//可以在j-1、j+1手里(加法原理)
 9 }
10 int ans=f[m][1];

经典例题:乌龟棋(Luogu 1541)

  乌龟棋的棋盘是一行 N 个格子,每个格子上一个分数(非负整数)。棋盘第 1 格是唯一的起点,第 N 格是终点,游戏要求玩家控制一个乌龟棋子从起点出发走到终点。
  乌龟棋中 M 张爬行卡片,分成 4 种不同的类型( M 张卡片中不一定包含所有 4 种类型的卡片,见样例),每种类型的卡片上分别标有 1、 2、 3、4 四个数字之一,表示使用这种卡片后,乌龟棋子将向前爬行相应的格子数。游戏中,玩家每次需要从所有的爬行卡片中选择一张之前没有使用过的爬行卡片,控制乌龟棋子前进相应的格子数,每张卡片只能使用一次。
  游戏中,乌龟棋子自动获得起点格子的分数,并且在后续的爬行中每到达一个格子,就得到该格子相应的分数。玩家最终游戏得分就是乌龟棋子从起点到终点过程中到过的所有格子的分数总和。

- 棋盘: 6 10 14 2 8 8 18 5 17
- 卡片: 1 3 1 2 1
- 答案: 73 = 6+10+14+8+18+17
很明显,用不同的爬行卡片使用顺序会使得最终游戏的得分不同,小明想要找到一种卡片使用顺序使得最终游戏得分最多。现在,告诉你棋盘上每个格子的分数和所有的爬行卡片,你能告诉小明,他最多能得到多少分吗?

* 思考:如何进行搜索?状态该如何设计?
* DFS(x,c1,c2,c3,c4) 为当前在第 x 个格子上, ci 代表标有数字i 的卡片有多少张。
* 于是可以直接写出状态 F[i][a][b][c][d],与状态是一一对应的,表示该状态下, 最大的权值是多少(当前第i个格子,四种牌分别用了i,j,k,l张的情况下,能达到的最优值)。
* 于是,可以和 F[i-1],F[i-2]…进行联系

 1 //T27:乌龟棋(我把i省略了,其实没什么用,加上只是用来判断是否越界而已)
 2 for(int i=0;i<=a;i++)
 3        for(int j=0;j<=b;j++)
 4           for(int k=0;k<=c;k++)
 5              for(int l=0;l<=d;l++)
 6                 {
 7                     if(i!=0)f[i][j][k][l]=max(f[i][j][k][l],f[i-1][j][k][l]);
 8                     if(j!=0)f[i][j][k][l]=max(f[i][j][k][l],f[i][j-1][k][l]);
 9                     if(k!=0)f[i][j][k][l]=max(f[i][j][k][l],f[i][j][k-1][l]);
10                     if(l!=0)f[i][j][k][l]=max(f[i][j][k][l],f[i][j][k][l-1]);
11                     f[i][j][k][l]+=s[i+j*2+k*3+l*4];
12                 }
时间: 2024-10-05 07:18:25

2017清北学堂集训笔记——动态规划Part2的相关文章

2017清北学堂集训笔记——图论

我们进入一个新的模块——图论! emmmmm这个专题更出来可能有点慢别介意,原因是要划的图和要给代码加的注释比较多,更重要的就是...这几个晚上我在追剧!!我们的少年时代超级超级超级好看,剧情很燃啊!!咳咳,好吧下面回归正题. 一.图的存储: 1.邻接矩阵: 假设有n个节点,建立一个n×n的矩阵,第i号节点能到达第j号节点就将[i][j]标记为1(有权值标记为权值), 样例如下图: 1 /*无向图,无权值*/ 2 int a[MAXN][MAXN];//邻接矩阵 3 int x,y;//两座城市

暑假清北学堂集训笔记

day -1 订票订得晚只好坐凌晨1点的火车-- day 0 7点钟到北京了,坐滴滴到酒店,然后去华北电力大学报道,路上看到一辆共享单车,弄了大半天才发现是坏的... 报完到就在旁边的餐厅吃了起来. day 1 南小鸟(钟皓曦)讲 搜索 分治 倍增 贪心 ST表: f[i][j]表示 从i到i+2^j-1这2^j个位置的元素最大值 初始化: f[i][0]=z[i] 转移: f[i][j]=max(f[i][j-1],f[i+2^(j-1)][j-1]) LCA(最近公公祖先):f[i][j]

清北学堂2017NOIP冬令营入学测试 P4744 A’s problem(a)

清北学堂2017NOIP冬令营入学测试 P4744 A's problem(a) 时间: 1000ms / 空间: 655360KiB / Java类名: Main 背景 冬令营入学测试题,每三天结算一次成绩.参与享优惠 描述 这是一道有背景的题目,小A也是一个有故事的人.但可惜的是这里纸张太小,小A无法把故事详细地说给大家听.可能小A自己也讲不清楚自己的故事,因为如果讲清了,也就没有这道题目了-- 小A的问题是这个样子,它找到了n份不同的工作,第i份工作每个月有ai的工资,每份工作需要小A每天

铁轨 清北学堂 线段树

铁轨 清北学堂 线段树 [题目描述] R 国的铁轨经常会进行重新修建. R 国是一个细长的国家,一共有 n 个城市排成一排,首都位于 1 号城市,相邻两个城市之间有铁路相连. 每次新建铁轨的时候,一定是从首都开始修建,直到某一个城市为止,这其间的铁路都会变成新版本的设 施,而旧设施会被拆除.然而,由于 R 国的工程师脑子不太好使,任意两种不同版本的铁路之间都无法连 接,因此必须要进行换乘. 现在给出你修建铁轨的操作,小 R 时不时第会想问你,如果在第 x 个城市到第 y 个城市之间随机选择一个

2017.7.21夏令营清北学堂解题报告

预计分数: 60+30+0=90=划水 实际分数: 90+30+20=140=rank5=雷蛇鼠标 一句话总结:今天该买彩票! T1: 题目描述 从前有一个?行?列的网格. 现在有?种颜色,第?种颜色可以涂??格,保证 Σ? ?? = ? * ?. 需要你对这个网格图进行着色,你必须按照从上到下,每一行内从左到右 的顺序进行着色,并且在用完一种颜色前你不能换颜色(当然颜色的使用顺序 是随意的). 每个相邻的相同色块可以获得1分,问在给定的规则下进行着色所能获得的 最高分是多少. 多组数据. 输入

【模板】 递归线段树 [2017年五月计划 清北学堂51精英班Day4]

P3372 [模板]线段树 1 题目描述 如题,已知一个数列,你需要进行下面两种操作: 1.将某区间每一个数加上x 2.求出某区间每一个数的和 输入输出格式 输入格式: 第一行包含两个整数N.M,分别表示该数列数字的个数和操作的总个数. 第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值. 接下来M行每行包含3或4个整数,表示一个操作,具体如下: 操作1: 格式:1 x y k 含义:将区间[x,y]内每个数加上k 操作2: 格式:2 x y 含义:输出区间[x,y]内每个数的

洛谷P1080 [NOIP2012提高组D1T2]国王游戏 [2017年5月计划 清北学堂51精英班Day1]

P1080 国王游戏 题目描述 恰逢 H 国国庆,国王邀请 n 位大臣来玩一个有奖游戏.首先,他让每个大臣在左.右 手上面分别写下一个整数,国王自己也在左.右手上各写一个整数.然后,让这 n 位大臣排 成一排,国王站在队伍的最前面.排好队后,所有的大臣都会获得国王奖赏的若干金币,每 位大臣获得的金币数分别是:排在该大臣前面的所有人的左手上的数的乘积除以他自己右 手上的数,然后向下取整得到的结果. 国王不希望某一个大臣获得特别多的奖赏,所以他想请你帮他重新安排一下队伍的顺序, 使得获得奖赏最多的大

【模板】倍增LCA [2017年5月计划 清北学堂51精英班 Day3]

P3379 [模板]最近公共祖先(LCA) 题目描述 如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先. 输入输出格式 输入格式: 第一行包含三个正整数N.M.S,分别表示树的结点个数.询问的个数和树根结点的序号. 接下来N-1行每行包含两个正整数x.y,表示x结点和y结点之间有一条直接连接的边(数据保证可以构成树). 接下来M行每行包含两个正整数a.b,表示询问a结点和b结点的最近公共祖先. 输出格式: 输出包含M行,每行包含一个正整数,依次为每一个询问的结果. 输入输出样例 输入

【模板】tyvjP1520 树的直径 [2017年5月计划 清北学堂Day3]

P1520 树的直径 时间: 1000ms / 空间: 131072KiB / Java类名: Main 描述 树的直径,即这棵树中距离最远的两个结点的距离.每两个相邻的结点的距离为1,即父亲结点与儿子结点或儿子结点与父子结点之间的距离为1.有趣的是,从树 的任意一个结点a出发,走到距离最远的结点b,再从结点b出发,能够走的最远距离,就是树的直径.树中相邻两个结点的距离为1.你的任务是:给定一棵树, 求这棵树中距离最远的两个结点的距离. 输入格式 输入共n行第一行是一个正整数n,表示这棵树的结点