【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! 我们通过飞线与线头的同时移动来达到像上图一样的路径。从而得到最优解。
观察发现,对于每一个点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[i−1,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[i−1,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[i−1,s[i],j](j̸=s[i])) 上面两条状态都比较容易理解,这条就有点困难了。这里的 g [ i − 1 ] [ s [ i ] ] [ j ] g[i-1][s[i]][j] g[i−1][s[i]][j]表示的是: 可以观察到,这里i与i+1刚好被经过了1次,所以满足f的定义。 注意: g [ i − 1 ] [ s [ i ] ] [ j ] g[i-1][s[i]][j] g[i−1][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[i−1,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[i−1,j]+3(j̸=s[i])) 这一条转移看上去也很诡异。为什么是正确的呢? 这条语句也极其重要:因为它代表着新建线头! 我们思考 f [ i − 1 ] [ j ] f[i-1][j] f[i−1][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[i−1,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[i−1,j,k]+1(j̸=s[i]且k̸=s[i])) 重点之三:延长线头! g [ i − 1 ] [ j ] [ k ] + 1 g[i-1][j][k]+1 g[i−1][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[i−1,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[i−1,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[i−1,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[i−1][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)
