zoukankan      html  css  js  c++  java
  • HDU5470 Typewriter SAM 动态规划 单调队列

    原文链接https://www.cnblogs.com/zhouzhendong/p/HDU5470.html

    题目传送门 - HDU5470

    题意

      你需要写一个只包含小写字母的字符串 $s$ 。

      你有两种操作:

      1. 在当前写好的字符串的末尾加上一个字符 $c$ ,代价是 $cost_c$ ,所有的 $cost_c$ 都会给出。

      2. 在已经写好的字符串中,选择一段子串 $a$ ,复制,并粘贴一次。代价是 $A imes |a|+B imes 2$ 。其中 $A$ 和 $B$ 会给出。

      问你写完字符串的最少花费是多少。

      多组数据,$Tleq 100,|s|leq 10^5,sum |s| leq 1.2 imes 10^6, 1leq A,B,cost_cleq 10^9$ 

    题解

      首先,我们看到这种涉及子串和末尾加上字符的题目,自然想到 SAM ,所以先建一个 SAM 再说。

      我们令 $dp_i$ 表示写完前 $i$ 个字符的最小花费。则每一个 $dp_i$ 有两种转移方式,一种是直接加字符,一种是复制粘贴。直接加字符的转移很容易,但是复制粘贴的转移方案很多。

      我们考虑一个 $dp_i$ 通过复制粘贴对后面的 $dp$ 值的贡献。有一个很显然的结论:它所能贡献的区域一定是一个连续段。考虑从第 $i+1$ 个位置开始取串,在由第 $i$ 个前缀构成的 SAM 上面走,得到能匹配的最长串,则比它短的显然都能匹配。我们再分析一下贡献的性质:贡献为 串长 × A + 2 × B 。是一个等差数列,我们显然可以线段树搞一下。

      现在我们回到之前,考虑如何求那个区间。区间下界显然是 $0$ ,那么问题在求区间上界,以第 $i$ 个字符为左端点,开始与第 $i-1$ 个前缀的子串匹配,我们记最远匹配点为 $R_i$ 。首先我们预处理一下每一个节点的 $right$ 集合中最小的位置,设为 $Time_i$,这样只要走到的节点 $x$ 满足 $Time_x<y$ ,我们就可以在第 $y-1$ 个前缀找到相应的匹配串。再有一个显然的结论,$R_i$ 是单调不降的。因为只需要截掉当前匹配串的第一个字母,就可以转移到下一个位置的匹配,而且是一定匹配的。所以我们可以考虑双指针单调扫一遍,把所有的 $R_i$ 都算出来。

      既然 $R_i$ 是单调的,而且我们需要区间更新 min 的等差数列数列的公差都是 $A$ ,所以我们可以考虑单调队列优化一下,这样就不需要写线段树了。

    代码

    #include <bits/stdc++.h>
    using namespace std;
    typedef long long LL;
    const int N=100005;
    int T,n,Case=0;
    int R[N],c[26],A,B;
    int Time[N<<1],tax[N],id[N<<1];
    char s[N];
    LL dp[N],v[N],q[N],head,tail;
    struct SAM{
    	int Next[26],fa,Max;
    }t[N<<1];
    int size;
    void init(){
    	memset(t,0,sizeof t);
    	memset(Time,63,sizeof Time);
    	size=1,t[0].Max=-1;
    	for (int i=0;i<26;i++)
    		t[0].Next[i]=1;
    }
    int extend(int p,int c){
    	if (t[p].Next[c]&&t[p].Max+1==t[t[p].Next[c]].Max)
    		return t[p].Next[c];
    	int np=++size,q,nq;
    	t[np].Max=t[p].Max+1;
    	for (;!t[p].Next[c];p=t[p].fa)
    		t[p].Next[c]=np;
    	q=t[p].Next[c];
    	if (t[p].Max+1==t[q].Max)
    		t[np].fa=q;
    	else {
    		nq=++size;
    		t[nq]=t[q],t[nq].Max=t[p].Max+1;
    		t[np].fa=t[q].fa=nq;
    		for (;t[p].Next[c]==q;p=t[p].fa)
    			t[p].Next[c]=nq;
    	}
    	return np;
    }
    void Get_Time(){
    	memset(tax,0,sizeof tax);
    	for (int i=1;i<=size;i++)
    		tax[t[i].Max]++;
    	for (int i=1;i<=n;i++)
    		tax[i]+=tax[i-1];
    	for (int i=1;i<=size;i++)
    		id[tax[t[i].Max]--]=i;
    	for (int i=size;i>1;i--)
    		Time[t[id[i]].fa]=min(Time[t[id[i]].fa],Time[id[i]]);
    }
    int main(){
    	scanf("%d",&T);
    	while (T--){
    		scanf("%s",s+1);
    		n=strlen(s+1);
    		init();
    		for (int i=1,p=1;i<=n;i++)
    			Time[p=extend(p,s[i]-'a')]=i;
    		Get_Time();
    		for (int i=0;i<26;i++)
    			scanf("%d",&c[i]);
    		scanf("%d%d",&A,&B);
    		for (int i=1,j=0,p=1,len=0;i<=n;i++){
    			while (1){
    				int Next=t[p].Next[s[j+1]-'a'];
    				while (!(Next&&Time[Next]<i)&&j-t[t[p].fa].Max<i)
    					p=t[p].fa,len=t[p].Max,Next=t[p].Next[s[j+1]-'a'];
    				if (Next&&Time[Next]<i)
    					j++,p=Next;
    				else
    					break;
    			}
    			R[i]=j;
    		}
    		head=1,tail=0;
    		memset(q,0,sizeof q);
    		dp[0]=0;
    		for (int i=1;i<=n;i++){
    			dp[i]=dp[i-1]+c[s[i]-'a'];
    			while (head<=tail&&R[q[head]+1]<i)
    				head++;
    			if (head<=tail)
    				dp[i]=min(dp[i],v[q[head]]+1LL*A*i+B*2);
    			v[i]=dp[i]-1LL*A*i;
    			if (R[i+1]>i){
    				while (head<=tail&&v[i]<=v[q[tail]])
    					tail--;
    				q[++tail]=i;
    			}
    		}
    		printf("Case #%d: %lld
    ",++Case,dp[n]);
    	}
    	return 0;
    }
    

      

  • 相关阅读:
    什么是 FutureTask?使用 ExecutorService 启动任务?
    WeakHashMap 是怎么工作的?
    什么是 Executors 框架?
    什么是原子操作?在 Java Concurrency API 中有哪些原 子类(atomic classes)?
    Java 中是如何支持正则表达式操作的?
    JDBC 能否处理 Blob 和 Clob?
    Java 中你怎样唤醒一个阻塞的线程?
    Java Concurrency API 中的 Lock 接口(Lock interface) 是什么?对比同步它有什么优势?
    为什么我们调用 start()方法时会执行 run()方法,为什么 我们不能直接调用 run()方法?
    什么是线程组,为什么在 Java 中不推荐使用?
  • 原文地址:https://www.cnblogs.com/zhouzhendong/p/HDU5470.html
Copyright © 2011-2022 走看看