【Codeforces-67A】Partial Teacher
上周刷了一大堆小紫薯的动态规划的题?……老师给我们布置了一场测试,感觉还好吧……虽然我并不是这么擅长动态规划?
◇ 题目
<手写翻译>
老师要给n个学生发糖,n个学生排成一队。由于老师有偏见?,对于一对相邻的学生,他会给成绩好的学生多一些糖(严格大于),而成绩好的学生少一些糖(严格小于),若相邻学生成绩相等,那么他们得到的糖的数量也是一样的。每个学生都至少得到一颗糖。求老师最少总共要给学生多少糖,输出给最少的糖的方案。
<输入输出>
单组数据。第一行输入n,表示学生的数量,学生编号为1~n,且1~n从左到右排列。第二行一个字符串描述了相邻学生的成绩关系,第i个字符描述的是第i个学生和和第i+1个学生的成绩关系,L表示第i个学生的成绩更好,R表示第i+1个学生的成绩更好,=表示两人成绩相等。
<样例&没解释?>
第一组数据 | 第二组数据 | ||
Input | Output | Input | Output |
5 | 2 1 2 1 2 | 5 | 1 1 2 3 4 |
◇ 解析
这道题其实是一道比较普通的DP,但是也有一点特别,至于是什么之后再讲。
让我们先忽略数据规模。
碰到这种题,我的第一想法就是暴力DP,所以就定义了一个非常暴力的状态:dp[i][j]表示第i个学生得到j颗糖,前i个学生得到的最少糖数。然后又加上了一个非常暴力的状态转移,用填表法,按照第i个和第i-1个学生成绩的关系分为3类——
①若第i个学生成绩好,则第i个学生分到的糖大于第i-1个学生的糖,即应从dp[i-1][小于j]转移过来,则有 dp[i][j]=min{dp[i-1][1~j-1]}+j;
②若第i-1个学生成绩好,则第i个学生分到的糖小于第i-1个学生的糖,即应从dp[i-1][大于j]转移过来,则有 dp[i][j]=min{dp[i-1][j+1~n]}+j;
③若两人成绩相等,则直接得到 dp[i][j]=dp[i-1][j]+j;
初始化时将dp[1][i]初始化为i,因为第一个学生没法与前一个学生对比。
于是我就得到了下面的伪代码:
for(枚举i,2~n)
for(枚举j,1~n)
{
if(第i个学生成绩好)
for(枚举k,1~j-1) 转移;
if(第i-1个学生成绩好)
for(枚举k,j+1~n) 转移;
if(两人成绩相等)
转移;
}
可见这是一个 O(n3)的算法,再看一眼数据规模……n≤1000。这样肯定会TLE的,于是我就没敢提交?。
是状态定义错误吗?好像其他状态定义没法转移……于是我开始想一些优化!
成绩相等的情况下是不需要优化的,本来就是O(n2)的,唯一需要优化的就是剩下的两种情况。注意到我们的转移,一种情况是从大于j的状态转移,另一种是从小于j的状态转移。于是我想到定义一个Min,初值为正无穷INF,当从大于j的状态转移时,表示的是 dp[i-1][j+1~n] 的最小值;当从小于j的状态转移时,表示的是 dp[i-1][1~j-1] 的最小值。
由于我们求 dp[i][j] 时,j的枚举顺序是对答案没有影响的,我进行了如下操作:
①若第i个学生成绩好,将j从小到大枚举,并在求dp[i][j]之前更新Min为 Min=min(Min,dp[i-1][j-1]),这样下来,Min的值就是 dp[i-1][1~j-1] 的最小值了,于是我们就可以改变转移式为 dp[i][j]=Min+j,瞬间少了O(n)(一层循环)的复杂度?;
②若第i-1个学生成绩好,将j从大到小枚举,并在求dp[i][j]之前更新Min为min(Min,dp[i-1][j+1]),按照之前的思路,转移式简化为 dp[i][j]=Min+j。
于是就可以非常愉快地进行DP了!!
但是题目要求的是输出方案,这是递推式dp的难点。我们存储一个fa[i][j]对应dp[i][j],表示的是 dp[i][j] 的状态值是由 dp[i-1][fa[i][j]] 转移过来的(由于第一维状态始终是由i-1转移而来,第一维不需要存储)。
那么我们如何找到 fa[i][j] ?由于 dp[i][j] 始终是从最小值,也就是 Min 转移过来,所以我们再定义一个变量Fa,表示当前状态是由 dp[i-1][Fa] 转移来的,在更新Min时,若Min被另一个值更新,则更新Fa为其第二维,转移时同时将 fa[i][j] 的值改为 Fa。
我们的最终答案是 dp[n][1~n] 转移过来的,我们定义pos为 dp[n][1~n] 中的最小值的第二维。所以我们可以递归输出答案,从 (n,pos) 开始,利用fa[i][j]倒推回 i=1 。
具体见代码~
◇ 源代码
/*Lucky_Glass*/
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXN=1000;
int n,INF;
char gra[MAXN+5]; //学生成绩关系
int dp[MAXN+5][MAXN+5],fa[MAXN+5][MAXN+5]; //dp和fa一一对应
int ans,pos;
void Print(int x,int y){ //递归输出
if(x<0) return; //全部结束
Print(x-1,fa[x][y]);
if(x>0) printf(" "); //处理空格
printf("%d",y);
}
int main(){
scanf("%d%s",&n,gra+1); //gra从1开始
memset(dp,0x3f,sizeof dp); //初始化为最大值
INF=dp[0][0];
ans=INF;
for(int i=1;i<=n;i++) dp[0][i]=i; //这里我把学生的编号改为0开始,也就是0~n-1
for(int i=1;i<n;i++)
{
int Min,Fa;
switch(gra[i])
{
case '=': //直接转移
for(int j=1;j<=n;j++)
{
dp[i][j]=dp[i-1][j];
fa[i][j]=j;
if(i==n-1) //最后的状态,找寻答案
if(ans>dp[i][j])
ans=dp[i][j],
pos=j; //记录第二维
}
break;
case 'R':
Min=INF;
for(int j=1;j<=n;j++) //顺序枚举
{
if(Min>dp[i-1][j-1]) //更新Min
{
Fa=j-1; //同时更新Fa
Min=dp[i-1][j-1];
}
if(dp[i][j]>Min+j) //转移
dp[i][j]=Min+j,
fa[i][j]=Fa;
if(i==n-1)
if(ans>dp[i][j])
ans=dp[i][j],
pos=j;
}
break;
case 'L':
Min=INF;
for(int j=n;j>=1;j--) //逆序枚举
{
if(Min>dp[i-1][j+1])
{
Fa=j+1;
Min=dp[i-1][j+1];
}
if(dp[i][j]>Min+j)
dp[i][j]=Min+j,
fa[i][j]=Fa;
if(i==n-1)
if(ans>dp[i][j])
ans=dp[i][j],
pos=j;
}
break;
}
}
Print(n-1,pos); //从最后的状态递归输出
return 0;
}
The End
Thanks for reading!
- Lucky_Glass