【BOI2013】文本编辑器——全面理解线头DP

it2022-05-08  7

【BOI2013】文本编辑器 (Standard IO) Time Limits: 2000 ms Memory Limits: 262144 KB Detailed Limits

Description Input

第一行包含了整数N,表示该文档的长度。下一行包含N个字符,每一个都是十个小写字母“a”到“j”之一。输入的第一个和最后一个字母都不是“e”。

Output

输出一个整数,表示Victor需要删除所有的“e”最少的按键次数。

Sample Input

35

chefeddiefedjeffeachbigagedegghehad

Sample Output

36

解释:其中一个最优方案是:fdhxhhxffhxfahxhhhxhhhxfdhxfghxfahhx

Data Constraint

50%数据:N ≤ 500

另外10%的数据:N ≤ 5000

100%的数据:N ≤ 70 000

题目大意

本题可以转化为,求一种路线进行方案。每一次可以从一个点向后跳到另一个点,然后从这个点往回走到一个点(也可以不走)之后再跳到下一个点。每一次的跳跃与行进都有特定的代价。求经过所有必经点的最小代价和。 很容易想到,每一次往前走都不能到上一次飞跃的落脚点以前。也就是 这样跳一定不是最优的。如果你能理解这个,就可以继续向下看了

算法介绍

线头DP 就是像滚线团一样,最终的形态为:

这个就是线头DP的大致状态(DP的转移路线) 我们要做的,不是像正常的DP一样去模拟这些路线,而是要通过像是穿针引线一样的将这些路径组合起来。 我们可以形象的理解为每一次飞跃都是一条飞线,而每一个向前走的路径都是一段粗线,粗线两端由线头组成。而且所有的线都是相连的! 飞一条线的代价为2,而引出一条长为1的粗选代价为1! 我们通过飞线与线头的同时移动来达到像上图一样的路径。从而得到最优解。

具体DP

观察发现,对于每一个点i与i+1.其中被线覆盖(飞线或粗线)的次数一定为1或3. 于是我们 设 f [ i ] [ j ] f[i][j] f[i][j]表示i与i+1两个点直接有且只有被覆盖了1次(飞线覆盖),飞线到达的右端点的字母j(s[k]==j) 的最小代价 设 g [ i ] [ j ] [ k ] g[i][j][k] g[i][j][k]表示i与i+1两个点有且只有被覆盖了3次(飞线与粗线同时覆盖),飞线到达的右端点的字母为j(s[p]==j)且粗线的右端点引出的飞线的右端点的字母为k(s[q]==k) 的最小代价。 注意!这两个状态中的线不一定是全部相连的!!也就是转移过程中求出来的答案不是正确的答案,这点很重要!! 也就是说,只要是被覆盖的次数满足条件,就可以计入方程,其他的全部不用考虑!!(这就是线头DP最迷的地方) 然后就是DP的转移式子了。 一共由10条转移,我们逐条来解释 1、 f [ i ] [ j ] = m i n ( f [ i ] [ j ] , f [ i − 1 , j ] ( j ≠ s [ i ] 且 i 点 不 是 必 须 点 ) ) f[i][j]=min(f[i][j],f[i-1,j](j≠s[i] 且i点不是必须点)) f[i][j]=min(f[i][j],f[i1,j](j̸=s[i]i)) 如果i-1 与 i 被一条飞线越过,这说明这条飞线很有可能也跨过了i与i+1。所以直接转移就可以。 2、 f [ i ] [ j ] = m i n ( f [ i ] [ j ] , f [ i − 1 , s [ i ] ] + 2 ) f[i][j]=min(f[i][j],f[i-1,s[i]]+2) f[i][j]=min(f[i][j],f[i1,s[i]]+2) 如果越过i-1与i的飞线恰好停在了i这个点,为了使得i与i+1被跨过一次,我们可以再飞出一条线,使得重新满足状态 f [ i ] [ j ] f[i][j] f[i][j] 的定义。 3、 f [ i ] [ j ] = m i n ( f [ i ] [ j ] , g [ i − 1 , s [ i ] , j ] ( j ≠ s [ i ] ) ) f[i][j]=min(f[i][j],g[i-1,s[i],j] (j≠s[i])) f[i][j]=min(f[i][j],g[i1,s[i],j](j̸=s[i])) 上面两条状态都比较容易理解,这条就有点困难了。这里的 g [ i − 1 ] [ s [ i ] ] [ j ] g[i-1][s[i]][j] g[i1][s[i]][j]表示的是: 可以观察到,这里i与i+1刚好被经过了1次,所以满足f的定义。 注意: g [ i − 1 ] [ s [ i ] ] [ j ] g[i-1][s[i]][j] g[i1][s[i]][j] 中的状态一定i与i-1的连边的代价一定是已经计算过的。 (思考,i-1往前的边的代价是否都已经计算过了呢?) 4、 f [ i ] [ j ] = m i n ( f [ i ] [ j ] , g [ i − 1 , s [ i ] , s [ i ] ] + 2 ) f[i][j]=min(f[i][j],g[i-1,s[i],s[i]]+2) f[i][j]=min(f[i][j],g[i1,s[i],s[i]]+2) 这里的情况就与第2种比较相像了。可以直接类比思考。由于篇幅有限,这里就不做阐述。 5、 g [ i ] [ j ] = m i n ( g [ i ] [ j ] , f [ i − 1 , j ] + 3 ( j ≠ s [ i ] ) ) g[i][j]=min(g[i][j],f[i-1,j]+3 (j≠s[i])) g[i][j]=min(g[i][j],f[i1,j]+3(j̸=s[i])) 这一条转移看上去也很诡异。为什么是正确的呢? 这条语句也极其重要:因为它代表着新建线头! 我们思考 f [ i − 1 ] [ j ] f[i-1][j] f[i1][j] 所表示的具体含义: 然后我们给这个图加一个线头:变成这样: 这里所需的代价为1. 然后为了保证i与i+1被覆盖3次,我们再将i与k(s[k]==j)相连: 这里,红色的边表示新加入的边。 这样就满足了g的定义。 6、 g [ i ] [ j ] = m i n ( g [ i ] [ j ] , f [ i − 1 , s [ i ] ] + 5 ) g[i][j]=min(g[i][j],f[i-1,s[i]]+5) g[i][j]=min(g[i][j],f[i1,s[i]]+5) 这里的转移与第2与第5个转移比较相像,就不过多阐述。 7、 g [ i ] [ j ] = m i n ( g [ i ] [ j ] , g [ i − 1 , j , k ] + 1 ( j ≠ s [ i ] 且 k ≠ s [ i ] ) ) g[i][j]=min(g[i][j],g[i-1,j,k]+1(j≠s[i] 且 k ≠s[i])) g[i][j]=min(g[i][j],g[i1,j,k]+1(j̸=s[i]k̸=s[i])) 重点之三:延长线头! g [ i − 1 ] [ j ] [ k ] + 1 g[i-1][j][k]+1 g[i1][j][k]+1 表示 红色的线表示增加的代价。 满足g的定义。 8、 g [ i ] [ j ] = m i n ( g [ i ] [ j ] , g [ i − 1 , s [ i ] , k ] + 3 ( k ≠ s [ i ] ) ) g[i][j]=min(g[i][j],g[i-1,s[i],k]+3 (k ≠s[i])) g[i][j]=min(g[i][j],g[i1,s[i],k]+3(k̸=s[i])) 9、 g [ i ] [ j ] = m i n ( g [ i ] [ j ] , g [ i − 1 , j , s [ i ] ] + 3 ( j ≠ s [ i ] ) ) g[i][j]=min(g[i][j],g[i-1,j,s[i]]+3 (j ≠s[i])) g[i][j]=min(g[i][j],g[i1,j,s[i]]+3(j̸=s[i])) 10、 g [ i ] [ j ] = m i n ( g [ i ] [ j ] , g [ i − 1 , s [ i ] , s [ i ] ] + 5 ) g[i][j]=min(g[i][j],g[i-1,s[i],s[i]]+5) g[i][j]=min(g[i][j],g[i1,s[i],s[i]]+5) 以上三种都是对第7种情况的拓展,拓展方式与第二个十分相像。就不过多阐述了。

正确性

DP的正确性如何保证呢? 我们发现线头DP中的链接情况总是十分混乱,这使得我们很难去分析它的正确性。 但是,我们发现这里有一个延长线头的操作。对于每一次新建完线头之后,由于不断的g数组本身的dp的积累,每一次对g数组的转移都意味着将线头向右进行延伸。由于所有的i都会参与到g数组的转移,所以所有的线头最终都会与飞线链接起来。就是说,所有的g中, g [ i − 1 ] [ j ] [ k ] g[i-1][j][k] g[i1][j][k]中,i-1到往前所到的点的代价一定都是计算过的。这也意味着当i移动到第n个的时候,所有的线头都一定是链接的代价都一定是计算过的。于是线头DP就没有了。

实现细节

引用一下YYT的题解 1、初始化f[0,s[1]]=0; 2、假设我们的字符集aj编号为09,那么我们将10视为一个没出现过的字符 最后dp出的ans就是f[n,10]-2 因为10不存在,所以可以把f[n,10]视作将绳子拉向一个无限远的点 又因为这样代价为2,而实际上我们并不需要这样做,所以减去2 3、当一个点是e,而我们是不需要管它的(因为要删)。 为了方便起见,我们就将这个点的f和g赋值为上一个点的(memcpy)

代码

#include<cstdio> #include<cstring> #define maxn 70001 #define N 11 int f[maxn][N],g[maxn][N][N],must[maxn]; int js=0,n; char s[maxn]; int min(int x,int y) { if (x<y) return x; return y; } int main() { int i,j,k,d; scanf("%d\n",&n); scanf("%s",s+1); memset(must,false,sizeof(must)); int ans=0; for(i=1;i<=n;i++) { if (s[i]=='e') ans+=2; if (s[i-1]=='e') must[i]=true; } memset(f,20,sizeof(f)); memset(g,20,sizeof(g)); f[0][s[1]-'a']=0; for (int i=1;i<=n;i++) { if (s[i]=='e') { memcpy(f[i],f[i-1],sizeof(f[i])); memcpy(g[i],g[i-1],sizeof(g[i])); } else { for (char j1='a';j1<='k';j1++) { int j=j1-'a'; f[i][j]=1e9; if((j1!=s[i])&&(!must[i])) f[i][j]=min(f[i][j],f[i-1][j]); f[i][j]=min(f[i][j],f[i-1][s[i]-'a']+2); if(j1!=s[i]) f[i][j]=min(f[i][j],g[i-1][s[i]-'a'][j]); f[i][j]=min(f[i][j],g[i-1][s[i]-'a'][s[i]-'a']+2); for (char k1='a';k1<='k';k1++) { int k=k1-'a'; g[i][j][k]=1e9; if(j1!=s[i]) g[i][j][k]=min(g[i][j][k],f[i-1][j]+3); g[i][j][k]=min(g[i][j][k],f[i-1][s[i]-'a']+5); if(j1!=s[i]&&k1!=s[i]) g[i][j][k]=min(g[i][j][k],g[i-1][j][k]+1); if(j1!=s[i]) g[i][j][k]=min(g[i][j][k],g[i-1][j][s[i]-'a']+3); if(k1!=s[i]) g[i][j][k]=min(g[i][j][k],g[i-1][s[i]-'a'][k]+3); g[i][j][k]=min(g[i][j][k],g[i-1][s[i]-'a'][s[i]-'a']+5); } } } } printf("%d",f[n]['k'-'a']+ans-2); return 0; }

最新回复(0)