小清新简单dp题(一)


1. P1020 [NOIP1999 普及组] 导弹拦截

经典题目。

由题意得,第一问需要求最长不下降子序列,第二问需要求最长上升子序列(每一个较大值可以炸到自己后面到下一个较大值之间的一段区间)。

那么怎么求这两个序列呢?

拿最长上升子序列为例,设 a 为原数组,长度为 nd 数组存储上升子序列,长度为 m

考虑枚举 a 数组的每一项,如果 ai>dm,则恰好可以成为 d 数组的一员,即 dm+1=ai

而如果 aidm,则我们可以选择 d 数组中第一个大于 ai 的项,将该项改为 ai

为什么这样正确呢? 本质上其实是一个贪心的思想。

不妨设 d 数组第一个大于 ai 的项为 dx

首先如果 x=m,则由于 ai<dx,将 dx 换成 ai 一定比不换更优(下一个 a 进入数组的门槛更小)。

如果 x<m,那么首先 dx 已经没用了,换不换都行,其次的话如果把 dx 换成 ai,考虑 dx<ai<ai+1<dx+1,本来 ai+1 要换掉 ai 的,将 dx 换成 ai 后,ai+1 只能换掉 dx+1,那么多换几次,最终还是可能换到 dm 的,有使答案变优的可能,何乐而不为?

至于为什么替换第一个大于 ai 的项?废话,替换第二个就打破 d 数组的单调递增的性质了。

那么怎么求 d 数组中第一个大于 ai 的数 dx 的下标呢?

考虑 lower_bound 和 upperbound 数组即可。

所以转移的代码如下:

for (int i=2;i<n;i++){
	if (d2[ans2]<a[i]) d2[++ans2]=a[i];
	else d2[lower_bound(d2+1,d2+1+ans2,a[i])-d2]=a[i];
}

求最长不下降子序列同理。


2. UVA10891 Game of Sum

很容易可以看出这是一道 区间 DP 的题目。

考虑设 dpi,j 为序列剩余 ij 时先手的最优解,则显然可以得到 dpi,j 的状态转移方程为:

dpi,j=sum(i,j)min(dpi,j1,dpi,j2,,dpi,i,dpi+1,j,dpi+2,j,,dpj,j)

然后很容易想到用 前缀和+差分 的方式预处理出 sum(i,j),然后先枚举区间长度,再枚举左端点右端点,最后枚举所有区间内 dp 数组的最小值。

复杂度 O(n3),虽然可以过,但显然不够优。

考虑区间内 dp 数组的最小值完全可以通过递推 O(1) 得到。设 zi,j=min(dpi,j,dpi,j1,dpi,j2,,dpi,i)yi,j=min(dpi,j,dpi+1,j,dpi+2,j,dpj,j)

则每次处理完 dpi,j 时,我们都可以顺便维护 zi,jyi,j

则新的状态转移方程式为

dpi,j=sumjsumi1min(0,zi,j1,yi+1,j)

其中可以取 0 是因为可以将这段数全部取走。

复杂度 O(n2)

int main(){
	while (1){
		if ((n=read())==0) break;
		for (int i=1;i<=n;i++) dp[i][i]=z[i][i]=y[i][i]=a=read(),sum[i]=sum[i-1]+a;
		for (int i=2;i<=n;i++){
			for (int l=1;l<=n-i+1;l++){
				int r=l+i-1;
				int x=min(0,min(z[l][r-1],y[l+1][r]));
				dp[l][r]=sum[r]-sum[l-1]-x;
				z[l][r]=min(z[l][r-1],dp[l][r]);
				y[l][r]=min(y[l+1][r],dp[l][r]);
			}
		}
		printf("%d\n",2*dp[1][n]-sum[n]);
	}
	return 0;
}

3. P1220 关路灯

由于我们可以将问题转化为任意一个包含起点的子区间走完后所需要的功耗,走完子区间后可以选择继续沿原来方向走下一个或者掉头走另一边的下一个,所以我们可以设计状态为 dpi,j,k,表示区间 [i,j] 走完后在区间左边或者在区间右边时的最小功耗,其中 k01 表示走完该区间后在区间左侧还是右侧。

那么转移方程就很好推了!

对于 dpi,j,0 可以是从区间 [i+1,j] 的左侧走一格到的,也可以是从右侧走 ji 格到的,那么转移的时候加上所需时间和未走点灯的功耗和之积即可。

dpi,j,1 同理,可以是从区间 [i,j1] 向右走一格得到,也可以是从左侧向右走 j,i 格得到。

最后输出 dp1,n,0dp1,n,1 的最小值即可。

int main(){
	n=read(),s=read();
	for (int i=1;i<=n;i++){
		a[i]=read();
		int b=read();
		sum[i]=sum[i-1]+b;
	}
	for (int i=1;i<=n;i++)
		for (int j=i;j<=n;j++)
			dp[i][j][0]=dp[i][j][1]=999999999;
	dp[s][s][0]=dp[s][s][1]=0;
	for (int l=2;l<=n;l++){
		for (int i=1;i<=n-l+1;i++){
			int j=i+l-1;
			dp[i][j][0]=min(dp[i+1][j][0]+(a[i+1]-a[i])*(sum[n]+sum[i]-sum[j]),
							dp[i+1][j][1]+(a[j]-a[i])*(sum[n]+sum[i]-sum[j]));
			dp[i][j][1]=min(dp[i][j-1][1]+(a[j]-a[j-1])*(sum[n]+sum[i-1]-sum[j-1]),
							dp[i][j-1][0]+(a[j]-a[i])*(sum[n]+sum[i-1]-sum[j-1]));
		}
	}
	printf("%d\n",min(dp[1][n][0],dp[1][n][1]));
	return 0;
} 

4. P3052 [USACO12MAR]Cows in a Skyscraper G

考虑用二进制集合表示每一个物品,其中集合的第 i 位表示第 i 个物品是否在集合当中。

我们用 dpi 来表示当集合状态为 i 时最少需要的分组数。

考虑到每一个集合 i 都可以由它的两个子集组成,我们可以列出如下状态转移方程:

dpi=min(dpt+1)

其中 ti 的一个子集。

但是这存在一个问题:集合 i 和集合 t 之间相差的元素的重量和不一定小于等于 W,所以我们需要特判一下。考虑枚举 i 的子集 j,如果 j 包含的元素的重量和在 W 以内,我们就可以令 t=i xor j,再执行上述转移方程即可。

为了快速计算一个集合所包含的元素的重量和,我们可以先预处理出每一种集合的重量和,用数组 sum 储存。重量和的计算可以用递归完成,即:

sumi=wlgi&i+sumi xor (i&i)

其中 lg2k 等于 k,可以通过预处理完成。

至此,我们就可以解决这道题了。最后输出 dp2n1 即可。

状态转移过程代码:

for (int i=1;i<(1<<n);i++)
	for (int j=i;j;j=(j-1)&i)
		if (sum[j]<=W)
			dp[i]=min(dp[i],dp[i^j]+1);

5. P2704 [NOI2001] 炮兵阵地

首先观察数据范围,m15,是状压 DP 无疑。

老样子,考虑将每一行缩成一个二进制数单独处理。由于每一门炮只会对上下两行产生影响,所以我们可以用 dpi,S,L 表示当我们处理到第 i 行,第 i 行状态是 S,第 i1 行状态是 L 时炮兵最多放置的数量。很明显,在满足约束条件的情况下,状态转移方程为:

dpi,S,L=max(dpi1,L,FL+totS)

其中 FL 表示的是第 i2 行的状态,totX 表示当一行状态为 X 时所包含的炮兵数量。

所以考虑先处理出前两行放置后最多的炮兵数量。分别枚举两行的状态即可。注意特判一下约束条件。

for (int i=0;i<(1<<m);i++)
	for (int j=0;j<(1<<m);j++){//枚举两行状态
		if (i&j||i&ma[1]||j&ma[2]||i&(i<<1)||i&(i<<2)||j&(j<<1)||j&(j<<2)) continue;
		//两行同列元素不能冲突
		//两行元素和地图不能冲突
		//行内炮兵不能冲突(和左移一位和两位的状态都不能冲突)
		dp[2][j][i]=tot[i]+tot[j]; //修改答案
	}

tot 数组的处理如:

int getsum(int x){//找二进制数x中1的个数
	int ans=0;
	while (x){
		if (x&1) ans++;
		x>>=1;
	}
	return ans;
}

//主函数内
for (int i=1;i<(1<<m);i++)
	tot[i]=getsum(i);

然后转移开始步入正轨,分别枚举每一行,枚举该行、该行的上一行、该行的上上行的状态,剔除冲突后按照转移方程转移即可。

注意,剔除冲突时需要枚举一个剔除一次,早剔除早降低复杂度。

又由于只有最近三(两)行的状态对目前有用,所以为了降低时间复杂度,我们可以采用滚动数组的方式,只记录有用的状态即可。

代码,如下:

for (int i=3;i<=n;i++)
	for (int c=0;c<(1<<m);c++)
		if (c&ma[i]||c&(c<<1)||c&(c<<2)) continue;
		for (int l=0;l<(1<<m);l++){
			if (l&c||l&(l<<1)||l&(l<<2)||l&ma[i-1]) continue;
			for (int fl=0;fl<(1<<m);fl++){
				if (fl&(fl<<1)||fl&(fl<<2)||c&fl||fl&l||fl&ma[i-2]) continue;
				dp[i%3][c][l]=max(dp[i%3][c][l],dp[(i-1)%3][l][fl]+tot[c]);
			}
		}

最后由于最后一行的每种状态都有可能成为答案,所以枚举最后一行的每一个 dp 数组的答案取最大值即可。


6. P1613 跑路

题目中带有 2k 字眼的 DP 大概率就是倍增 DP 吧!

考虑到 n 比较小,为了获得较为详细的数据,Floyd 显然是我们的不二选择。

考虑对于 disi,j,如果 disi,kdisk,j 都可以成为 2k1,那么 disi,j 显然也可以成为 2k,此时距离就变成了 1

所以我们可以设置一个 bool 型数组 cani,j,p,表示 disi,j 可否成为 2p

考虑程序流程,首先毫无疑问,dis 数组的每一项都应该设为极大值。

输入一条边 (u,v) 时,disu,v 显然应该设为 1,而又由于 120 次方,则 canu,v,0=1

然后枚举每一个 p,进行正常的 Floyd 程序,处理 can 数组的每一项,如果 cani,k,p1=cank,j,p1,则将 cani,j,p 设为 1 后,再将 disi,j 设为 1 即可。

最后跑一遍正常的 Floyd 即可求出答案。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define N 70
ll n,m,dis[N][N];
bool can[N][N][N];
ll read(){
	ll w=0,f=1;
	char ch=getchar();
	while (ch>'9'||ch<'0'){
		if (ch=='-') f=-1;
		ch=getchar();
	}
	while (ch>='0'&&ch<='9'){
		w=(w<<3)+(w<<1)+(ch^48);
		ch=getchar();
	}
	return w*f;
}

int main(){
	n=read(),m=read();
	memset(dis,63,sizeof(dis));
	for (int i=1;i<=m;i++){
		int u=read(),v=read();
		dis[u][v]=can[u][v][0]=1;
	}
	for (int p=1;p<=63;p++){
		for (int k=1;k<=n;k++){
			for (int i=1;i<=n;i++){
				for (int j=1;j<=n;j++){
					if (can[i][k][p-1]==1&&can[k][j][p-1]==1)
						dis[i][j]=1,can[i][j][p]=1;
				}
			}
		}
	}
	for (int k=1;k<=n;k++){
		for (int i=1;i<=n;i++){
			for (int j=1;j<=n;j++){
				dis[i][j]=min(dis[i][k]+dis[k][j],dis[i][j]);
			} 
		}
	}
	printf("%lld\n",dis[1][n]);
	return 0;
}

7. P2657 [SCOI2009] windy 数

首先为什么这是一道数位 DP 题呢?

观察到题目中的两个条件:

  • 相邻两个数字之差至少为 2.
  • ab 之间有多少个符合条件的数。

简单来说可以概括为【询问范围内符合条件的数】,这也基本是数位 DP 的主要特征了。

那么数位 DP 题怎么做呢?

解数位 DP 题的过程可以简单概括为:

  • 设状态——赋初值——从高到低按位转移——统计答案

下面我们拿本题举例。先考虑求 1n 之间 Windy 数的数量。

先说第一条设状态,在数位 DP 题中,我们一般用 dpi.j,k 来表示已经处理完了第 i 到第 s 位,第 i 位是 j 时的 Windy 数总数,k 是一个 bool 型,1 表示目前维护到的这个数已经保证小于 n0 表示目前来看这个数的前 i 位都和 n 相同。这种设计状态的方式一般可以推广到所有数位 DP 的题目。i 的表示也可以反过来)

然后我们考虑转移。设 ai 表示 ni 位上的数(设 n 的个位为第一位)对于上一位还没有确定是否小于等于 n 的状态,如果从第 i 位开始确定了,那么第 i 位的数一定只能小于 ai,即:

dpi,j,1+=dpi+1,ai+1,0,0j<ai

如果第 i 位也还没有确定,则第 i 位只能是 ai,所以 dpi,ai,0 继承 dpi+1,ai+1,0 的状态,即:

dpi,ai,0=dpi+1,ai+1,0

如果前 i+1 位已经小于 n,那么第 i 位可以选择 09 的任何一个数,加上第 i+1 位选择 09 任何一个数的方案总数,即:

dpi,j,1+=dpi+1,k,1

对了,记得在加的时候加上限制条件,特判剔除掉相邻两位之差小于等于 2 的情况。

状态转移说完了,新的问题又来了。既然是求方案总数,肯定要将 dp 数组的一部分赋上初值 1。这道题如何赋值呢?

首先,如果第 s 位等于 as,那么目前无法确定小于 n,则 dps,as,0=1

而如果第 s 位小于 as,则目前确定小于 n,又因为不能有前缀 0,所以 j0,即:

dps,j,1=1,1j<ai

还有一种情况:如果该数的位数已经比 n 小,则大小肯定比 n 小,又因为前缀同样不能有 0,故:

dpi,j,1=1,1i<s,1j9

最后统计答案时将所有的 dp1,j,1,0j9 加起来即可,再加上 dp1,a1,0 即为答案。


8. P2602 [ZJOI2010]数字计数

题目让我们求 ab 之间的每个数字的个数,老样子,为了去掉下界,我们可以转化成分别求 0a10b 之间的数字个数,然后相减即可。

那么怎么求 1n 之间每个数字的个数呢?

考虑将程序分为两步:转移答案汇总答案

  1. 转移答案

考虑设 dpi,j,k 表示当我们处理到第 i 位,最高位是 j 时,数字 k 的个数。

那么转移也很好想到,即:

dpi,j,k=p=09dpi1,p,k

而由于在上面的式子中,我们的每一个数都没有包含最高位的数字,所以需要在加上最高位数字的贡献,即:

dpi,j,j+=10i1

(以 j 为最高位时每一个数都包含 j

如此我们就把所有的答案转移了出来。

  1. 统计答案

考虑用 ansi 来表示第 i 个数字的个数,设要求 1n 之间每个数字的个数,n 的长度为 l,第 i 位为 ai

首先所有位数小于 ndp 数组的贡献肯定要记入答案(注意最高位不能为 0),即

ansk+=i=1l1j=19dpi,j,k

其次位数与 n 相同,但最高位小于 n 的最高位的 dp 数组的贡献也要记入答案,即

ansk+=j=1an1dpl,j,k

剩下的 dp 我们设 [i+1,n]n 完全相同,则我们从 0ai1 枚举每一个第 i 位,计入贡献。而且由于 [i+1,n] 位相同,这些数字记得也要计入贡献。

这部分代码为:

for (int i=1;i<l;i++)
	for (int j=1;j<=9;j++)
		for (int k=0;k<=9;k++)
			ans[k][pd]+=dp[i][j][k];
for (int i=1;i<a[l];i++)
	for (int k=0;k<=9;k++)
		ans[k][pd]+=dp[l][i][k];
for (int i=l-1;i>=1;i--){
	for (int j=0;j<a[i];j++)
		for (int k=0;k<=9;k++)
			ans[k][pd]+=dp[i][j][k];
	for (int p=l;p>i;p--) ans[a[p]][pd]+=a[i]*ksm(10,i-1);
}
posted @   ydtz  阅读(87)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示