小清新简单dp题(三)

17. P3411 序列变换

竟然自己想出来了正解!!!!1

很容易想到一个结论:n 次操作之内必然可以使序列变为不降。

显然可以做到:只需要从小到大将每一个元素依次移动到数列结尾即可。

这个结论有什么用呢?没什么用,不过我们可以顺着继续向下想。

如果将第 n 大的元素移动到数列末尾,在最优情况下,是不是就意味着不会再移动任何数到数列末尾了?否则一定还需要将该元素再移动到末尾一次,显然不优。

同理,如果将第 n1 大的元素移动到数列末尾,那么会且仅会将第 n 大的元素移动到末尾。移动到开头的规则也同理。

而剩下的是哪些元素呢?显然是一段大小连续且上升的元素。

所以我们就成功地将题目转化为了:求出序列中的最长无缝不下降子序列。

这个就很好求了,不妨设 dpi 表示当前以大小为 i 的元素作为结尾的最长无缝不下降子序列,如果前面已经出现了 i,则:

dpi=dpi+1

如果 i 从来没出现过,则有:

dpi=dpi1+1

注意需要提前将原数组离散化。答案即为序列长度减去 dp 数组每一项的最大值。

for (int i=1;i<=n;i++){
	if (!dp[a[i]]) dp[a[i]]=dp[a[i]-1]+1;
	else dp[a[i]]++;
	ans=max(dp[a[i]],ans);
}

update:假了,但是题解就是这么写的,懒得更新((


18. P3486 [POI2009]KON-Ticket Inspector

数据结构优化 dp。

注意到,题目中询问的是不同的人的最多票数。

考虑设 dpi,k 表示前 i 个车站检票 k 次且要在车站 i 检票时最多能检到的不同的人的票数。发现其实每个人的上下车轨迹其实都是一段区间,而当我们从决策 j 转移过来的时候,相当于第 k 次检票会检到从区间 [j+1,i] 上车,从区间 [i+1,n] 下车的人的个数。

那么本题就很明了了。注意到暴力枚举人数复杂度会很高,于是采用二维前缀和优化即可。

时间复杂度 O(n2k)

for (int i=0;i<=n;i++)
	dp[i][0]=0;
for (int i=1;i<=n;i++)
	for (int j=0;j<i;j++)
		for (int s=1;s<=k;s++){
			int next=dp[j][s-1]+sum[i][n]-sum[j][n]-sum[i][i]+sum[j][i];
			if (next>dp[i][s]){
				dp[i][s]=next;
				f[i][s]=j;
			}
		}

19. P2079 烛光晚餐

套路背包题。

注意到其实本题有三个维度,小明的喜爱程度、小红的喜爱程度以及体积。

其中小明的喜爱程度与体积是限制维,而小红的喜爱程度是优化维。

于是考虑将小明的喜爱程度和体积作为下标,将小红的喜爱程度作为内容,进行0-1背包转移。

最后输出所有小明喜爱程度大于 0 的 dp 最大值即可。

注意小明的喜爱程度可能为负数,需要将其整体向右位移 500 作为真正下标。

dp[0][500]=0;
for (int i=1;i<=n;i++){
	int c=read(),x=read(),y=read();
	for (int j=v;j>=c;j--)
		for (int k=1000;k>=x;k--)
			dp[j][k]=max(dp[j][k],dp[j-c][k-x]+y);
}
int ans=-1;
for (int i=1;i<=v;i++)
	for (int j=500;j<=1000;j++)
		ans=max(ans,dp[i][j]);

20. P2606 [ZJOI2010]排列计数

将所有的 ii×2i×2+1 连边,发现构成了一棵树。

根据题目中的条件,树的祖先在排列中的位置要先于后代。

于是就可以转化为一个树形的组合计数问题。对于以 i 为根的一棵子树,i 自身分配的排列序号一定是最小的,左子树会取以 i 为根的子树内 sizelsoni 个编号,共有 Csizelsoni+sizersonisizelsoni 种可能。左子树取完后右子树得到取值自然就固定下来了。

而左右子树内部又会再次分配取值,所以 dpi=dplsoni×dprsoni×C.

不过注意到 m 不一定小于 n,所以不能直接求组合数,需要使用 Lucas 定理解决。

ll C(ll x,ll y){
	if (y>x) return 0;
	return jc[x]*ksm(jc[y],m-2,m)%m*ksm(jc[x-y],m-2,m)%m;
}
ll lucas(ll x,ll y){
	if (y==0) return 1;
	return C(x%m,y%m)*lucas(x/m,y/m)%m;
}
ll solve(ll x){
	if (x>n) return 1;
	ll ans=solve(x<<1)*solve(x<<1|1)%m*lucas(size[x<<1]+size[x<<1|1],size[x<<1])%m;
	return ans;
}

21. P2625 豪华游轮

可以将操作分类,注意到开始的角度向前走一定是最优的,所以可以先将所有向前走的步数在一开始的时候走完。

然后贪心地来看,我们需要通过旋转找到一个最优秀的角度,然后一次性将所有后退的步数走完,如果还有角度剩余,则放到最后转即可。

找最适合后退的角度的过程相当于什么?没错,可以转化为一个背包问题。

dpi,j 表示对于前 i 个旋转是否能转到角度 j,然后进行转移即可。

注意最后需要将角度值转化为弧度制,然后用三角函数公式算出最终移动的距离。

最终的计算代码如下:

#define pai acos(-1)//acos为cos的逆运算,cosΠ=-1
for (int i=0;i<=180;i++){
	if (dp[m][180+i]||dp[m][180-i]){
		double d=180-i;
		printf("%.6lf\n",sqrt(ans*ans+ba*ba-2*ans*ba*cos(d*pai/180)));
		return 0;
	}
}

22. P3694 邦邦的大合唱站队

n 很大,m 很小,考虑将 m 状压。

注意到每一个乐队的成员一定是连续的一段,所以如果我们将乐队之间的排队顺序确定下来,我们就可以确定下来整个序列的样子。

由于是求顺序,且 m 很小,于是可以将已经选择的乐队状压。设 dpS 表示已经排好的乐队集合为 S,所需要交换的人数的最小值。

预处理出每个乐队人数的前缀和,然后枚举集合中包含的乐队进行转移即可。

dp[0]=0;
for (int i=1;i<(1<<m);i++){
	int now=solve(i);
	for (int j=1;j<=m;j++){
		if (i&(1<<(j-1))){
			int last=now-js[j];
			dp[i]=min(dp[i],dp[i^(1<<(j-1))]+now-last-sum[j][now]+sum[j][last]);
	
        }
	}
}

23. P2051 [AHOI2009] 中国象棋

标签状压 dp 是认真的吗???好吧也勉强算是状压。

题目相当于是让求出每一行象棋个数不超过 2,每一列个数也不超过 2 的方案个数。

正常的状压可以用 dpi,S 表示前 i 列中有一个炮或两个炮的列的集合为 S 的方案个数,可以过掉 50% 的数据。

但我们发现这种状压方式很弱,弱在哪呢?这种状压方式实际上是多维护了某一列有几个炮的。但事实上,这并不是我们需要关心的内容。转移时,我们只关心目前为止有几个位置有一个炮,几个位置有两个炮,而不是哪几个位置。

所以我们可以进一步优化状态,设 dpi,j,k 表示对于前 i 行,其中有 j 列有一个炮,k 列有两个炮的方案数,然后根据前一行的 dp 值处理组合数正常转移即可。

dp[0][0][0]=1;
for (int i=1;i<=n;i++){
	for (int j=0;j<=m;j++){
		for (int k=0;k<=m;k++){
			dp[i][j][k]=dp[i-1][j][k];
			if (j>=2) dp[i][j][k]=(dp[i][j][k]+dp[i-1][j-2][k]*C(m-k-j+2,2)%mod)%mod;//0->1 0->1
			if (k>=1) dp[i][j][k]=(dp[i][j][k]+dp[i-1][j][k-1]*C(m-j-k+1,1)%mod*C(j,1)%mod)%mod;//0->1 1->2
			if (k>=2) dp[i][j][k]=(dp[i][j][k]+dp[i-1][j+2][k-2]*C(j+2,2)%mod)%mod;//1->2 1->2
			if (k>=1) dp[i][j][k]=(dp[i][j][k]+dp[i-1][j+1][k-1]*C(j+1,1)%mod)%mod;//1->2
			if (j>=1) dp[i][j][k]=(dp[i][j][k]+dp[i-1][j-1][k]*C(m-k-j+1,1)%mod)%mod;//0->1
		}
	}
}

24. P4141 消失之物

非常常见的 trick。

​ 首先很容易想到最暴力的背包解法:对于每 n1 个物品跑一遍 0-1 背包,然后依次输出答案。时间复杂度 O(n2m).

这样很显然是不优的——对于每个物品我们都重复进行了 dp,浪费了许多效率。于是考虑优化。

可以联想到一个经典问题:求区间最大双段子段和

我们都会求普通的最大子段和,dp 或者前缀和处理等做法都可以将它完美解决。但是扩展到双段问题似乎就变得有些棘手——对于 dp,状态的设计将会变得十分复杂繁琐。

考虑一种更优美的解法,即从前到后和从后到前分别跑一遍 dp,求出前 i 项及后 i 项的最大子段和,然后枚举双段子段和的断点,将两遍 dp 结果拼凑起来取最大值即可。

这个解法看上去十分具有启发性。扩展的来看,如果我们可以通过从前到后递推出前 i 项的 dp 值,那么我们同样也可以求出后 i 项的 dp 值,将两者拼凑到一起则可以求出扩展到双段的相同问题的 dp 值。

再回来看本题。可以发现去除掉第 i 个物品的背包相当于就是前 i1 项以及后 ni 项背包拼凑到一起,不过此时我们还缺少背包的体积信息。所以可以考虑先正反分别跑一遍完整的背包,然后枚举断点和左侧体积,顺便推出右侧体积,将二者相乘后相加就可以求出每一个 cnt 的值。

for (int i=1;i<=n;i++){
	for (int j=1;j<=m;j++){
		int ans=0;
		for (int k=0;k<=j;k++)
			ans=(ans+dp[0][i-1][k]*dp[1][i+1][j-k]%10)%10;
		printf("%d",ans);
	}
	puts("");
}

然而这个做法并没有怎么优化复杂度。。。于是就假了

考虑正解。计数背包在求解的过程中,对于体积为 m 时的物品 i ,贡献其实为 dpmwi,所以我们在删除第 i 个物品时减去这个贡献即可。

不过要注意,在计算贡献的时候我们是倒序枚举的,所以在删除贡献的时候我们应该正序枚举。


25. CF1575L Longest Array Deconstruction

稍微简化一下题面,原题其实就是让你将 A 数组删去若干数得到 B 数组,使得 B 数组满足 ai=i 的项数尽可能的多。

很容易想到设 dpi,j 表示当前处理到第 i 个数,且前面一共删掉了 j 个数时最大的满足条件的项数。则有:

dpi,j=max(dpi1,j+[ai+j=i],dpi1,j1)

时空复杂度显然不行,让我们再优化。

我们发现对答案有贡献的条件只有一个,也就是 ai+j=i。所以其实我们的第二维的条件是有点浪费的。

考虑设 dpi 表示对于前 i 个数,第 i 个数被放在 ai 的位置上时的最大满足条件数(aii),此时我们有转移方程:

dpi=max(dpj+1),ji,aj<ai,aiiajj

于是就是喜闻乐见的最长上升子序列的问题了。


26. CF1442D Sum

由于 n 个数组不降,我们可以推出一个结论:我们至多只会有一个序列未取完;剩下的序列要不一个不取,要不全部取完。

这个结论很容易通过贪心+反证的方式进行证明,在此不再赘述。

然后我们就将该问题转化成了:

n1 个完整物品和一个部分物品放入背包,所能得到的最大价值。

乍一看此题和 T24 差不多,都是有一个物品受到影响,n1 个物品不受影响。

但仔细观察发现,T24 是计数类背包,加法具有可逆性;而本题却是普通的 0-1 背包,最大值的操作并不具有可逆性。于是 T24 的解法就泡汤了。

既然不具有可逆性,那么我们就只能考虑将 n1 个物品的答案拼起来;观察数据范围,这个拼起来的过程复杂度只能是 nlogn 级别的,毕竟我们还需要 O(n) 的时间来枚举剩下的那个物品的选择长度。

于是考虑分治。类似于线段树,我们可以用均摊 logn 级别的复杂度来拼凑出选择某个物品作为部分物品时的最优答案。

具体来说,对于一个区间 [l,r],考虑得到中点 mid 后,将编号在区间 [mid+1,r] 的物品加入背包,然后递归解决区间 [l,mid];回溯之后,将 dp 数组恢复到之前的状态,然后将区间 [l,mid] 的物品加入背包,再递归解决区间 [mid+1,r]。当左右端点相等时,枚举当前物品选择的部分,与背包中剩余体积的最大价值相加后取最大值即可。

注意元素个数的规模是 1e6 级别的,并不好直接储存;不过某个数组中 k 项之后的元素其实都是没有用的,所以我们在输入时将每个数组的长度与 k 取最小值,读到 k 项之后的元素不存入数组即可。

void solve(int o,int l,int r){
	if (l==r){
		for (int i=1;i<=min(p[l],k);i++) 
			ans=max(ans,sum[l][i]+dp[k-i]);
		return;
	}
	for (int i=0;i<=k;i++) zc[o][i]=dp[i];
	int mid=l+r>>1;
	for (int i=l;i<=mid;i++)
		for (int j=k;j>=p[i];j--)
			dp[j]=max(dp[j],dp[j-p[i]]+sum[i][p[i]]);
	solve(o+1,mid+1,r);
	for (int i=0;i<=k;i++) dp[i]=zc[o][i];
	for (int i=mid+1;i<=r;i++)
		for (int j=k;j>=p[i];j--)
			dp[j]=max(dp[j],dp[j-p[i]]+sum[i][p[i]]);
	solve(o+1,l,mid);
}
posted @   ydtz  阅读(70)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
点击右上角即可分享
微信分享提示