【题解】CCPC-Final 2019 C. Mr. Panda and Typewriter 后缀自动机+启发式合并,DP

链接:GYM 102431C

给定长为 n ( 5000 ) n(5000) 、字符集 1 e 9 1e9 的字符串,给定 x , y , z ( 1 e 9 ) x,y,z(1e9) ,问最少花多少时间能够打字打出这个字符串。关于打字的操作有三种:

  1. 花费 x x 时间,在末尾打出一个字符。
  2. 花费 y y 时间,复制已经打出的字符串的一个子串。(剪贴板改变)
  3. 花费 z z 时间,在末尾粘贴复制的字符串。(剪贴板不变)

动态规划

  1. 状态表示: d p [ i ] [ j ] dp[i][j] 表示打完前 i i 个字符、最后一步复制了长为 j j 的段的最小代价。

    1. j z / x j \ge z/x ,否则不如全手打
    2. j i / 2 j\le i/2 ,否则无法复制
    3. 特别地, j = 0 j=0 表示最后一个字符是手打出来的。
    4. m i [ i ] mi[i] 表示 j j 取任意合法值时的最小 d p [ i ] [ j ] dp[i][j]
    5. l s t [ i ] [ j ] lst[i][j] 表示满足 l s t i + j lst \le i+j s [ l s t j + 1 , l s t ] = s [ i j + 1 , i ] s[lst-j+1,lst]=s[i-j+1,i] 的最大位置,即最后一次不相交的出现位置。如果没有这样的位置, l s t [ i ] [ j ] = 0 lst[i][j]=0 .
  2. 状态转移:

    1. d p [ i ] [ 0 ] = m i [ i ] + x dp[i][0] = mi[i]+x ,表示手打一个字符
    2. d p [ i ] [ j ] = m i [ i j ] + y + z dp[i][j] = mi[i-j]+y+z ,表示当场复制一段再粘贴。
      需要满足 l s t [ i ] [ j ] > 0 lst[i][j]>0 ,即这个子串在之前不相交地出现过一次。
    3. d p [ i ] [ j ] = d p [ l s t [ i ] [ j ] ] [ j ] + x ( i j l s t [ i ] [ j ] ) + z dp[i][j] = dp[lst[i][j]][j]+x*(i-j-lst[i][j])+z ,表示使用之前的剪贴板。
      因为从上一次复制到这次复制之间剪贴板不会变化,所以中途的字符全部手动打出,最后只粘贴一次即可。
      需要满足 l s t [ l s t [ i ] [ j ] ] [ j ] > 0 lst[lst[i][j]][j]>0 ,即这个子串在之前不相交地出现过两次。
  3. 状态边界: d [ i ] [ j ] = i n f d p [ 0 ] [ 0 ] = 0 d[i][j]=inf,dp[0][0]=0 .

  4. 答案状态: m i [ n ] mi[n]

  5. 复杂度: l s t + O ( n 2 ) 求lst数组的复杂度+O(n^2)

只剩下一个问题,怎么求 l s t lst

后缀自动机,启发式合并,三指针

时刻注意,后缀自动机的每个节点对应一组长度连续且依次为后缀的字符串,它们在原字符串中具有相同的出现位置(endpos集)。

易证一个结论: Σ e n d p o s _ s i z e [ u ] ( l e n [ u ] l e n [ l i n k [ u ] ] ) = n ( n + 1 ) / 2 \Sigma endpos\_size[u]*(len[u]-len[link[u]]) = n*(n+1)/2 ,即每个节点的endpos集大小乘以字符串个数就是这个节点所有字符串的所有出现位置,所有节点的总和正好是子串个数。

现在 l s t lst 数组正好有 n ( n + 1 ) / 2 n*(n+1)/2 项,我们可以依次处理每个节点,求得每个endpos与每个字符串长度的 l s t lst .

为了获得节点的endpos集,可以先建立后缀自动机,然后使用启发式合并的方法依次处理每个节点。

遍历一个endpos集,对于最短的字符串长度,使用双指针求它的lst。每求得一个endpos的答案,就再添加一个指针,依次把字符串长度增大到最长,求得这个endpos的所有答案。

使用上面的结论可以证明,这样的做法复杂度总和是 O ( n 2 ) O(n^2) 的。

启发式合并还有 O ( n l o g 2 n ) O(nlog^2n) 的复杂度,不过加上之后总复杂度还是 O ( n 2 ) O(n^2)

总结与代码

  1. 这题多组数据多倍时限,memset没用好可能会超时。
  2. RE一次,因为dsu的部分忘了开双倍空间。
  3. 很多较难的题目,都是需要先猜结论的。大胆假设,小心求证。
  4. 系统总结了sam+dsu的写法,训练场上写了不少时间,这道题可以打印下来当模板备用。
  5. 双指针法的扩展,三指针法,细节思考了比较久时间。
  6. 获得了新结论,可以用来证明复杂度,训练场上计算了好长时间的时间复杂度,算不出来不敢写。
  7. 现场赛只有两个队过,AC的时候挺高兴的,后来看知乎上出题人说这题medium easy,TAT,不过确实不是特别难。
  8. 使用自动机的字符串题会自带小常数,很难卡掉,不要过度优化,勇敢地交吧!
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
inline int read()
{
	int x=0,f=1; char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
	return x*f;
}
const int M = 5016, MOD = 1000000007;

namespace sam
{
	int sz, lst; map<int, int> ch[M<<1];
	int len[M<<1], link[M<<1], pre[M<<1];
	int create(int id=0)
	{
		ch[id].clear();
		len[id] = link[id] = pre[id] = 0;
		return sz = id;
	}
	void extend(int n)
	{
		create(0); lst = 0; 
		for(int i=0; i<n; ++i)
		{
			int c = read(), cur = create(sz+1);
			len[cur] = len[lst] + 1;

			int p = lst;
			while(!ch[p][c])
			{
				ch[p][c] = cur;
				p = link[p];
			}
			if(ch[p][c] != cur)
			{
				int q = ch[p][c];
				if(len[p]+1 == len[q]) link[cur] = q;
				else
				{
					int clone = create(sz+1);
					ch[clone] = ch[q];
					link[clone] = link[q];
					len[clone] = len[p] + 1;
					while(ch[p][c]==q)
					{
						ch[p][c] = clone;
						p = link[p];
					}
					link[q] = link[cur] = clone;
				}
			}
			lst = cur;
			pre[cur] = i+1;
		}
	}
}
int csf[M][M]; //csf[i][j]表示以i为endpos、长为j的子串,最后一个小于等于j-i的endpos
namespace dsu
{
	using namespace sam;
	vector<int> son[M<<1]; //树
	set<int> st[M<<1]; int tar[M<<1]; 
	int tmp[M];

	void merge(int a, int b)
	{
		if(st[tar[a]].size()<st[tar[b]].size()) swap(a,b);
		for(auto x:st[tar[b]])
			st[tar[a]].insert(x);
		st[tar[b]].clear();
		tar[b] = tar[a];
	}
	void dfs(int u)
	{
		for(auto v:son[u])
			dfs(v), merge(u, v);
		//下方是启发式合并的实际处理,本题的终极目标是得到csf数组
		if(!u || !st[tar[u]].size()) return;
		int cnt = 0;
		for(auto x : st[tar[u]])
			tmp[cnt++] = x;
		int l1 = len[link[u]]+1, l2 = len[u];
		for(int i=1, j=-1; i<cnt; ++i) //i表示右侧指针,j表示让i,l1合法的最后一个指针
		{
			while(j+1<i && tmp[i]-tmp[j+1]>=l1) ++j;
			for(int k=j, l=l1; ~k && l<=l2; ++l) //k表示让i,l合法的最后一个指针
			{
				while(~k && tmp[i]-tmp[k]<l) --k;
				if(~k) csf[tmp[i]][l] = tmp[k];
			}
		}
	}
	void solve()
	{
		st[tar[0]].clear();
		for(int i=0; i<=sz; ++i)
		{
			son[i].clear();
			tar[i] = i;
			if(pre[i]) st[i].insert(pre[i]);
		}
		for(int i=1; i<=sz; ++i)
			son[link[i]].push_back(i);
		dfs(0);
	}
}
ll dp[M][M]; //dp[i][j]表示写完前i个字符的最后一步是粘贴了长为j的段,的最小花费。
ll mi[M]; //mi[i]表示dp[i][]的最小值
int main(void)
{
	#ifdef _LITTLEFALL_
	freopen("in.txt","r",stdin);
	#endif

	int T = read();
	for(int kase=1; kase<=T; ++kase)
	{
		int n = read(), x = read(), y = read(), z = read();
		memset(csf, 0, (n+1)*sizeof(csf[0]));
		memset(dp, 0x3f, (n+1)*sizeof(dp[0]));
		memset(mi, 0x3f, (n+1)*sizeof(mi[0]));
		sam::extend(n);
		dsu::solve(); //求得csf
		dp[0][0] = mi[0] = 0;
		for(int i=1; i<=n; ++i)
		{
			dp[i][0] = mi[i-1] + x;
			mi[i] = dp[i][0];
			for(int j=z/x; j<=i/2; ++j) if(csf[i][j])
			{
				dp[i][j] = mi[i-j] + y + z;
				if(csf[csf[i][j]][j])
					dp[i][j] = min(dp[i][j], dp[csf[i][j]][j] + 1ll*x*(i-j-csf[i][j]) + z);
				mi[i] = min(mi[i], dp[i][j]);
			}
		}
		printf("Case #%d: %I64d\n", kase, mi[n] );
	}


	return 0;
}
发布了375 篇原创文章 · 获赞 305 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/m0_37809890/article/details/103515142