1. 程式人生 > 其它 >[GKOI 2021 提高組]抄寫

[GKOI 2021 提高組]抄寫

抄寫copy

題目大意

對於 \(26\) 個英文字母,每個英文字母對應一個權值,對於任意小寫字母 \(ch\) ,我們稱抄寫它的花費為 \(cost_{ch}\)

當然,你有一種節約時間的方法,你可以花費時間 \(c\) ,將之前抄寫的字元對稱到另一邊,即如下兩種操作:

  • 操作一,在 \(s_m\) 後對稱,即使得抄寫進度變為 \(s_1s_2……s_ms_ms_{m-1}……s_k,1\leq k \leq m\)

  • 操作一,在 \(s_m\) 處對稱,即使得抄寫進度變為 \(s_1s_2……s_ms_{m-1}……s_k,1\leq k < m\)

現在給你一個長度為 \(n\)

的字串,問你抄寫它的最小花費。

分析

暴力解法

首先,這道題的 \(n^2\) 做法並不難想到,我們設 \(dp[i]\) 表示抄寫前 \(i\) 個字元的最小花費,顯然, \(dp[i]\) 有兩種被更新的方式。

\[dp[i]=min(dp[i-1]+cost[s[i]-'a'+1],dp[k]+c) \]

很顯然,這個 \(k\) 即我們的對稱點,如果不加優化的話,我們只能通過列舉去找,且判斷是否對稱又是一維,這樣我們的時間複雜度顯然是不優秀的。

我們考慮在每個點的時候更新後面的點,就是以當前更新到的點作為題面中的 \(s_m\) ,左右兩邊同時向外判斷,如果一直相同,就更新即可。

這樣我們的時間複雜度能夠達到 \(n^2\)

然後這題其實還有一點性質分,二分的做法很容易想到,這裡就不贅述了。

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+10,INF=1e10;
inline int read()
{
	int s=0,w=1;
	char ch=getchar();
	while(ch<'0'||ch>'9') { if(ch=='-') w*=-1; ch=getchar(); }
	while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar();
	return s*w;
}
int n,c;
int f[N],cost[N];
char s[N];
inline int find(int l,int r,char x)
{
	int res=-1;
	while(l<=r){
		int mid=(l+r)/2;
		if(s[mid]!=x) l=mid+1;
		else res=mid,r=mid-1;
	}
	return res;
}
//目測60分 
signed main()
{
//	freopen("copy.in","r",stdin);
//	freopen("copy.out","w",stdout);
	n=read(),c=read();
	for(register int i=1;i<=26;i++) cost[i]=read();
	for(register int i=1;i<=n;i++) s[i]=getchar();
	for(register int i=1;i<=n;i++) f[i]=INF;
	f[0]=0;
	if(n<=1000){ //40分 
		for(register int i=1;i<=n;i++){
			//cout<<i<<"\n";
			f[i]=min(f[i-1]+cost[s[i]-'a'+1],f[i]);
			int l=i,r=i+1;
			while(l>=1&&r<=n){ //操作1
				if(s[l]==s[r]){ //能夠關於其對稱 
					f[r]=min(f[r],f[i]+c);
					l--,r++;
				}
				else break;
			}
			l=i-1,r=i+1;
			while(l>=1&&r<=n){ //操作2 
				if(s[l]==s[r]){ //能夠關於其對稱 
					f[r]=min(f[r],f[i]+c);
					l--,r++;
				}
				else break;
			}
		}
		printf("%lld\n",f[n]);
	} 
	else{ //性質分20分
		for(register int i=1;i<=n;i++){
			//cout<<i<<"\n";
			int k=find(1,i-1,s[i]);
			if(k==-1) f[i]=f[i-1]+cost[s[i]-'a'+1];
			else{
				int mid=(i-k+1)/2;
				if((i-k+1)&1) mid+=1;
				f[i]=min(f[i-1]+cost[s[i]-'a'+1],f[k+mid-1]+c);
			}
		}
		printf("%lld\n",f[n]);
	}
	return 0;
}

正解(hash+二分+線段樹)

這是一種 \(nlogn\) 的解法,擴充套件 \(kmp\)\(manacher\) 等演算法也能做到。

想想怎麼優化我們的過程,通過列舉去找到最大的邊界然後一個一個更新,還是太暴力了。對於這種區間更新,線段樹是比較顯然的一種想法,但是我們如何在優秀的時間複雜度內找到迴文半徑。

我們可以把原來的字串正著 \(hash\) 一遍,倒著 \(hash\) 一遍,在二分迴文半徑即可。

這樣做的時間是 \(nlogn\)

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+10,INF=1e18;
inline int read()
{
	int s=0,w=1;
	char ch=getchar();
	while(ch<'0'||ch>'9') { if(ch=='-') w*=-1; ch=getchar(); }
	while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar();
	return s*w; 
}
int n,c;
int a[N],f[N],dp[N],tree[4*N]; //f[i]記錄第i個點通過迴文能夠更新到的點的最遠位置 
char s[N];
unsigned long long p[N],hash1[N],hash2[N];
inline void build(int k,int l,int r)
{
	if(l==r) { tree[k]=INF; return; }
	int mid=(l+r)/2;
	build(k*2,l,mid),build(k*2+1,mid+1,r);
	tree[k]=min(tree[k*2],tree[k*2+1]);
}
inline void change(int k,int l,int r,int x,int y,int v)
{
	if(l>y||r<x) return;
	if(l>=x&&r<=y) { tree[k]=min(tree[k],v); return; }
	int mid=(l+r)/2;
	change(k*2,l,mid,x,y,v),change(k*2+1,mid+1,r,x,y,v);
}
inline int ask(int k,int l,int r,int x)
{
	if(r<x||l>x) return INF;
	if(l==r) return tree[k];
	int mid=(l+r)/2;
	return min(tree[k],min(ask(k*2,l,mid,x),ask(k*2+1,mid+1,r,x)));
}
signed main()
{
	n=read(),c=read();
	for(register int i=1;i<=26;i++) a[i]=read();
	scanf("%s",s+1);
	p[0]=1;
	for(register int i=1;i<=n;i++){
		hash1[i]=hash1[i-1]*131+(s[i]-'a'+1);
		hash2[n-i+1]=hash2[n-i+2]*131+(s[n-i+1]-'a'+1);
		p[i]=p[i-1]*131;
	}
//	for(register int i=1;i<=n;i++) cout<<hash2[i]<<" ";
//	puts("");
	for(register int i=1;i<=n;i++){
		int l=0,r=min(i-1,n-i),res=-1;
		while(l<=r){ //奇數 
			int mid=(l+r)/2;
			if(hash1[i]-hash1[i-mid-1]*p[mid+1]==hash2[i]-hash2[i+mid+1]*p[mid+1]) res=mid,l=mid+1;
			else r=mid-1;
		}
		if(res!=-1) f[i]=max(f[i],i+res);
		l=0,r=min(i-1,n-i-1),res=-1;
		while(l<=r){ //偶數 
			int mid=(l+r)/2;
			if(hash1[i]-hash1[i-mid-1]*p[mid+1]==hash2[i+1]-hash2[i+mid+2]*p[mid+1]) res=mid,l=mid+1;
			else r=mid-1;
		}
		if(res!=-1) f[i]=max(f[i],i+res+1);
	}
	build(1,1,n);
	for(register int i=1;i<=n;i++){
		dp[i]=min(dp[i-1]+a[s[i]-'a'+1],ask(1,1,n,i));
		if(i+1>f[i]) continue;
		else change(1,1,n,i+1,f[i],dp[i]+c);
	}
	printf("%lld\n",dp[n]);
	return 0;
}
作者: ╰⋛⋋⊱๑落葉๑⊰⋌⋚╯

-------------------------------------------

海到無邊天作岸,山登絕頂我為峰!