DP 专项练习

[USACO23OPEN] Pareidolia S

对于这种题,两种思路,一种是直接 \(dp\),一种是考虑每个 bessie 产生的贡献。

显然直接考虑 bessie 产生的贡献难以解决 bbessie 的情况,所以考虑 \(dp\)

\(f_{i}\) 表示以 \(i\) 开头的字符串的总贡献,那么显然有 \(ans=\sum_{i=1}^{len}f_i\),考虑如何转移。

bessie 来划分,对于 \(i\),找到往后 \(e\) 的位置,记作 \(j\),那么这个 bessie 就会在后边产生 \(len-j+1\) 的贡献,同时再加上 \(f_{j+1}\) 即可。即:

\[f_{i}=f_{j+1}+len-j+1 \]

record

[USACO09OPEN] Grazing2 S

考虑直接 \(dp\),设 \(dp_{i,j}\) 表示前 \(i\) 个奶牛,最后一个放在 \(j\) 这个牛棚,的最小移动距离。

转移是显然的,直接对于每个 \(i\) 枚举 \(j\) 以及与 \(j\) 的距离不超过 \(d\) 的即可。

但是这样时间复杂度是 \(\rm O(NS)\),显然不行。

先将所有牛按照初始位置排序,对于第 \(i\) 头牛,其最前的位置显然位于 \(1+(i-1)\times d\),最后的位置位于 \(s-(n-i)\times d\),也就是说对于 \(i\) 只需要枚举这些位置进行转移即可,事件复杂度为 \(O(S)\),注意转移过程要用滚动数组优化。

const int N=1e6+10;
int n,s,a[N];
LL f[N],g[N];

int main() {
	cin>>n>>s;
	int d=(s-1)/(n-1);
	for(int i=1;i<=n;i++) cin>>a[i];
	sort(a+1,a+1+n);
	memset(f,0x3f,sizeof(f));
	memset(g,0x3f,sizeof(g));
	f[1]=g[1]=abs(a[1]-1);
	for(int i=2;i<=n;i++) {
		for(int j=1+(i-1)*d;j<=s-(n-i)*d;j++) {
			int p=j-d;
			f[j]=g[p]+abs(a[i]-j);
			if(p-1>=1+(i-2)*d) f[j]=min(f[j],g[p-1]+abs(a[i]-j));
		}
		for(int j=1+(i-1)*d;j<=s-(n-i)*d;j++) {
			g[j]=f[j];
		}
	}
	cout<<f[s];
	
	
	return 0;
}

[USACO16OPEN] 262144 P

对于这道题的弱化版,直接设 \(f_{i,j}\) 表示 \(i\sim j\) 这段期间合并出的最大数字,然后 \(\rm O(n^3)\) 转移即可。

但是这道题,不要说 \(O(n^3)\),就是连 \(O(n^2)\) 都过不了,这是我们就要转化状态设计。

原来我们将合并出来的数字当作答案,位置当作状态,不妨反过来,将合并出来的数字当作状态,位置当作答案,具体而言,设 \(f_{i,j}\) 表示以 \(i\) 开头,合并出数字 \(j\) 的右端点,显然右端点只有一个,所以不需熬考虑那么多。

考虑转移,由于两个相同的数字合并成一个大 \(1\) 的数字,所以有如下式子:

\[f_{i,j}=f_{f_{i,j-1}+1,j-1} \]

考虑最大的数字会合并出什么,由于两个数字可以变大 \(1\),所以最大的数字就是 \(40+\log N\) 最大是 \(60\),所以处理到 \(60\) 即可。

const int N=3e5,M=61;
int n,a[N];
int f[N][M];

int main() {
	cin>>n;
	for(int i=1;i<=n;i++) {
		int x; cin>>x;
		f[i][x]=i;
	}
	for(int i=1;i<=60;i++) {
		for(int j=1;j<=n;j++) {
			if(f[j][i]) continue;
			if(!f[j][i-1]) continue;
			f[j][i]=f[f[j][i-1]+1][i-1];
		}
	}
	for(int i=60;i>=1;i--) {
		for(int j=1;j<=n;j++) {
			if(f[j][i]) {
				cout<<i; return 0;
			}
		}
	}
	return 0;
}

[USACO04DEC] Cleaning Shifts S

直接设 \(f_i\) 表示前 \(i\) 分钟最少需要几头奶牛,也就是最少选几头奶牛才可以覆盖。

对于朴素算法,对于每个 \(i\),我们需要找到 \(E_j>=i\)\(S_j\) 最小的 \(j\),毕竟贪心的考虑,肯定是覆盖的越多越好。

首先将数组按照 \(E\) 从小到大排序,那么对于一个 \(i\),满足第一个条件的就是一个后缀,那么我们对每个后缀求一个 \(S\)\(minn\),如果这个 \(minn\) 仍然不符合第 \(2\) 个条件,说明无法转移,输出无解;否则直接用这个转移即可。

const int N=3e4,M=1e6+10;
int t,n;
int minn[N],f[M];
struct node {
	int s,e;
}a[N];

int cmp(node a,node b) {
	return a.e<b.e;
}

int main() {
	cin>>n>>t;
	memset(minn,0x3f,sizeof(minn));
	for(int i=1;i<=n;i++) {
		a[i].s=read();
		a[i].e=read();
	}
	sort(a+1,a+1+n,cmp);
	for(int i=n;i>=1;i--) minn[i]=min(minn[i+1],a[i].s);
	int p=1;
	memset(f,0x3f,sizeof(f));
	f[0]=0;
	for(int i=1;i<=t;i++) {
		while(a[p].e<i&&p<=n) p++;
		if(a[p].e<i||minn[p]>i) {
			cout<<-1;
			return 0;
		}
		f[i]=min(f[i],f[minn[p]-1]+1); 
	}
	cout<<f[t];
	
	return 0;
}

[USACO20OPEN] Exercise G

首先将题目描述进行抽象,不难发现,对于题目给出的 \(A=(2,3,1,5,4)\),就是下面这张图:

不难发现,对于左边这个环,需要转 \(3\) 次才能回到原来的形状,右边的环,需要转 \(2\) 次才 \(ok\),所以总的转数就是取最小公倍数,即 \(6\)

那么我们便可以将问题抽象成如下:将 \(n\) 分解为若干个数字之和,对这些数字取 \(\rm lcm\),问一共有多少种不同的 \(\rm lcm\) 个数。

如果直接对怎么拆分进行考虑,显然是不妥的,因为若现在拆分出来的数字和之前的有公因数,那么贡献很难计算,但如果我们单独对素数进行考虑,显然一个素数与其他数字(除非自己,这种情况后面再说)是没有公因数的,这样会方便转移,并且任何一个数皆可以表示为素数的乘积。

\(f_{i,j}\) 表示前 \(i\) 个数字,最大的素数是 \(j\) 的总拆分数,由于求的是种类数之和,所以我们直接钦定从小到大进行拆分。对于相同的数字,我们在一次转移中直接以指数幂的方式放进去,与之前的不重复即可。

为了转移方便,在维护一个数组 \(g_{i,j}\) 表示前 \(i\) 个数字,最大的素数不超过 \(j\) 的总数。这个数组在 \(f\) 数组计算完一次后前缀和维护即可。

有如下转移:

\[f_{i,j}=\sum({g_{i-pr_j^{k},j-1}+pr_j^k}) \]

const int N=1e4+10,M=1e4,L=1300;
int n,MOD;
int vis[N],m,pr[N];
LL f[N][L],g[N][L];

void shai() {
	pr[++m]=1;
	for(int i=2;i<=M;i++) {
		if(!vis[i]) pr[++m]=i;
		for(int j=2;j<=m&&i*pr[j]<=M;j++) {
			vis[pr[j]*i]=1;
			if(i%pr[j]==0) break;
		}
	}
} 

int main() {
	cin>>n>>MOD;
	shai();
	f[0][0]=1;
	for(int i=0;i<=m;i++) g[0][i]=1;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m&&pr[j]<=i;j++) {
			if(pr[j]==1) {
				f[i][j]=1;
			}
			else {
				for(int k=pr[j];k<=i;k*=pr[j]) {
					f[i][j]=(f[i][j]+g[i-k][j-1]*(LL)k%MOD)%MOD;
				}				
			}
		}
		for(int j=1;j<=m;j++) {
			g[i][j]=(g[i][j-1]+f[i][j])%MOD;
		}
	}
	LL ans=0;
	for(int i=1;i<=m;i++) {
		ans=(ans+f[n][i])%MOD;
	}
	cout<<ans;

	return 0;
}

[HNOI2004] 敲砖块

不难发现直接 \(dp\) 并不容易,不妨先找一找合法方案的规律。

可以发现,若选择了 \((i,j)\),那么 \((i,j)\) 往下的全部三角形都要选上。但是这样仍然存在问题,选择的两个点可能重合,也就是三角形区域有可能重合,造成贡献难以计算,也就是下图,但如果考虑仅仅保留三角形顶上的轮廓,应该说是不难转移的。

可以发现,轮廓线的走向要么直接向下,要么取右上,我们只需要按照转移写出方程即可。

const int N=52;
int n,m;
LL a[N][N],f[N][N][N*N],sum[N][N];

int main() {
	cin>>n>>m;
	for(int i=1;i<=n;i++) {
		for(int j=1;j<=n-i+1;j++) {
			cin>>a[i][j];
		}
	}
	memset(f,-0x3f,sizeof(f));
	for(int j=1;j<=n;j++) {
		f[1][j][1]=a[1][j];
		for(int i=1;i<=n-j+1;i++) sum[j][i]=sum[j][i-1]+a[i][j];
		for(int z=1;z<j;z++) {//需要处理中间没有轮廓线的情况。
			for(int k=2;k<=m;k++) {
				f[1][j][k]=max(f[1][j][k],f[1][z][k-1]+a[1][j]); 
			}
		}
		for(int i=1;i<=n-j+1;i++) {
			for(int k=1;k<=m;k++) {
				f[i][j][k]=max(f[i][j][k],f[i-1][j][k-1]+a[i][j]);
				if(k-i<0||j==1) continue;
				for(int z=min(i+1,n-j+2);z>=2;z--) {
					f[i][j][k]=max(f[i][j][k],f[z][j-1][k-i]+sum[j][i]);
				}
			}
		}
	}
	
	LL ans=0;
	for(int i=1;i<=n;i++) {
		for(int j=0;j<=m;j++)
			ans=max(ans,f[1][i][j]);
	}
	cout<<ans;
	return 0;
}

[JSOI2007] 重要的城市

对于 \(n\le 200\) 的数据结构,加上在最短路的背景下,基本可以确定是用类似 \(Floyed\) 的方式进行 \(dp\)
\(Floyed\) 的本质就是逐个加点,然后将这个点作为中转点取更新其他点,那么我们设想,如果一个点加入后,有两点之间的最短路马上被更新,一定可以说明这个点在当前时刻属于两点之间的必须经过点;那么如果更新的值与之前的相等,那么这两点之间没有必须经过的点,不设置中转城市。
最后只需要将所有点对的必过点拿出来,排序去重输出即可。

const int N=210;
int n,m;
LL f[N][N],ans[N][N],cnt,b[N*N];

int main() {
	cin>>n>>m;
	memset(f,0x3f,sizeof(f));
	for(int i=1;i<=n;i++) f[i][i]=0;
	for(int i=1;i<=m;i++) {
		int a,b; LL c; cin>>a>>b>>c;
		f[a][b]=min(f[a][b],c);
		f[b][a]=min(f[b][a],c);
	}
	for(int k=1;k<=n;k++) {
		for(int i=1;i<=n;i++) {
			for(int j=1;j<=n;j++){
				if(f[i][j]>f[i][k]+f[k][j]) {
					f[i][j]=f[i][k]+f[k][j];
					if(k!=i&&k!=j) ans[i][j]=k;
				}
				else if(f[i][j]==f[i][k]+f[k][j]) {
					if(k!=i&&k!=j) ans[i][j]=0;
				}
			}
		}
	}
	for(int i=1;i<=n;i++) {
		for(int j=1;j<=n;j++) {
			if(ans[i][j]) b[++cnt]=ans[i][j];
		}
	}
	sort(b+1,b+1+cnt);
	cnt=unique(b+1,b+1+cnt)-b-1;
	if(!cnt) cout<<"No important cities.";
	else {
		for(int i=1;i<=cnt;i++) cout<<b[i]<<" ";
	}

	return 0;
}
posted @ 2023-11-04 10:45  2017BeiJiang  阅读(14)  评论(0编辑  收藏  举报