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

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

Description
【BOI2013】
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的转移路线)
我们要做的,不是像正常的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) 的最小代价
f[i][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) 的最小代价。
g[i][j][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]表示的是:
g[i-1][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] 所表示的具体含义:
f[i-1][j]的转移一
然后我们给这个图加一个线头:变成这样:f[i-1][j]的转移二
这里所需的代价为1.
然后为了保证i与i+1被覆盖3次,我们再将i与k(s[k]==j)相连:
f[i-1][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;
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值