2025 暑假集训 Day10

2025.8.14

简单 DP 综合训练。

P4316 绿豆蛙的归宿

https://www.luogu.com.cn/problem/P4316

\(dp(u)\) 表示点 \(u\)\(n\) 的期望长度,然后考虑从终点出发倒回起点逆推,初始值 \(dp(n)=0\)

对于点 \(u\),有 \(k\) 条边 \((u,v_1),(u,v_2),\cdots,(u,v_k)\)\(u\) 为起点,则 \(u\) 在这些边中等概率地选取一个边走。每一条边有 \(\dfrac 1 k\) 的概率被选中,假设边 \((u,v_i)\) 被选中,期望长度是 \(dp(v)+w_i\)。所以有以下的转移方程:

\[dp(u) = \sum \dfrac{dp(v)+w(u,v)}{k} \]

然后把图里面每一条边反着建立然后跑一遍拓扑排序就可以了。

https://www.luogu.com.cn/record/230809336

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=1e5+7;
double dp[N];
int n,m,k[N];  //k统计出度
int rd[N];  //拓扑排序要用到的入度 
vector<pair<int,int>> g[N];
int main()
{
//	freopen("neuvillette.in","r",stdin);
//	freopen("neuvillette.out","w",stdout);
	cin>>n>>m;
	for(int i=1,u,v,w;i<=m;i++)
	{
		cin>>u>>v>>w;
		g[v].push_back({u,w});  //建反向图
		k[u]++;  //统计的是正向图的出度 
		rd[u]++;
	}
	dp[n]=0;
	queue<int> q;
	for(int i=1;i<=n;i++) if(rd[i]==0) q.push(i);
	while(!q.empty())
	{
		int u=q.front(); q.pop();
		for(auto f:g[u])
		{
			int v=f.first,w=f.second;
//			if(v==1) cerr<<v<<' '<<u<<' '<<w<<' '<<dp[u]<<' '<<k[v]<<endl;
			dp[v]+=((w+dp[u])*1.0/k[v]);
			if(!(--rd[v])) q.push(v);
		}
	}
//	for(int i=1;i<=n;i++) cerr<<fixed<<setprecision(2)<<dp[i]<<' ';
	cout<<fixed<<setprecision(2)<<dp[1];
	return 0;
}
/*
dp[i]表示从i到结点n的路径长度期望 
dp[u] <- dp[v]+(w/k)
*/

P1868 饥饿的奶牛

https://www.luogu.com.cn/problem/P1868

\(dp(i)\) 表示 \(1 \sim i\) 中选择任意个不重复的区间所吃到的最多的牧草数量,然后显然地有一下状态转移方程:

\[dp(j)=\max(dp[i])+(r[j]-l[j]+1)\qquad(i<j,r_i<l_j) \]

这个时间复杂度是 \(O(n^2)\) 的,显然过不了题面 \(n=150000\) 的数据。注意到时间复杂度的瓶颈在于每一次转移都要找一个合适的 \(i\) 转移给 \(j\),但是应该是只有一个最大的满足 \(i<j,r_i<l_j\) 转移过来(因为 \(dp(i)\) 代表的是 \(1 \sim i\) 中的答案,\(dp(i)\) 已经把 \(1 \sim i\) 的答案都包含了)

所以只需要找到一个最大的满足 \(i<j,r_i<l_j\)\(i\) 就好了。把 \(r_i\) 从小到大排序就有了单调性,然后就可以愉快地二分了,时间复杂度就从 \(O(n^2)\) 砍到了 \(O(n \log n)\)

https://www.luogu.com.cn/record/230885387

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=1.5e5+7;
struct node{int l,r;}a[N];
int dp[N],l[N],r[N],n;
int main()
{
//	freopen("neuvillette.in","r",stdin);
//	freopen("neuvillette.out","w",stdout);
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i].l>>a[i].r;
	sort(a+1,a+n+1,[&](node A,node B){return A.r<B.r;});
	for(int i=1;i<=n;i++) l[i]=a[i].l,r[i]=a[i].r;
	dp[1]=r[1]-l[1]+1;
	for(int j=2;j<=n;j++)
	{
		int i=lower_bound(r+1,r+(j-1)+1,l[j])-r-1;
//		if(j==12) cerr<<i<<endl;
		if(i!=0) dp[j]=max(dp[j-1],dp[i]+r[j]-l[j]+1);
		else dp[j]=max(dp[j-1],r[j]-l[j]+1);
	}
//	for(int i=1;i<=n;i++) cerr<<dp[i]<<' ';
	cout<<*max_element(dp+1,dp+n+1); 
	return 0;
}
/*
dp[i]表示1~i中选择任意个不重复的区间所吃到的最多的牧草数量
dp[i]=max(dp[j]+r[i]-l[i]+1)  其中j<i且l[i]>r[j] 

往后转移? 
dp[j]=max(dp[i])+r[j]-l[j]+1  其中i<j且r[i]<l[j] 
max(dp[i])能用线段树维护吗?好像不行? 
能从最近的一个i转移过来吗?好像可以?
一定可以,因为dp[i]表示的是1~i中的答案,i前面的答案被dp[i]包含了
所以可以二分出一个i然后转移了,优化到 O(n log n) 
要从r[1~j-1]中找一个最大的<l[i]的数 
*/

P2049 魔术棋子

https://www.luogu.com.cn/problem/P2049

这道题就相比前两题就良心一点了。观察到模数 \(K \le 100\),这就代表着模数只有 \(100\) 种可能,所以就可以设 \(dp(i,j,t)\) 表示从 \((1,1)\) 走到 \((i,j)\) 时走过的路径格子数的乘积 \(\bmod K=t\) 有没有可能(函数值为 \(\operatorname {true}\) 就是有可能,\(\operatorname{false}\) 就是没可能),然后就从 \((i-1,j),(i,j-1)\) 转移过来就好了。状态转移方程(\(a_{i,j}\) 表示棋盘中的数,\(\operatorname{or}\) 表示逻辑或运算):

\[dp(i,j,t \times a_{i,j} \bmod k)=dp(i-1,j,t) \operatorname{or} dp(i,j-1,t) \]

最后枚举一下 \(t=0 \sim k-1\) 输出一下答案就好了。

https://www.luogu.com.cn/record/230890133

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=107;
bool dp[N][N][N];
int m,n,k;
int a[N][N];
int main()
{
//	freopen("neuvillette.in","r",stdin);
//	freopen("neuvillette.out","w",stdout);
	cin>>n>>m>>k;
	for(int i=1;i<=n;i++)
	for(int j=1;j<=m;j++) cin>>a[i][j];
	dp[1][1][a[1][1]%k]=1;
	for(int i=1;i<=n;i++)
	for(int j=1;j<=m;j++)
	for(int t=0;t<k;t++)
	{
		dp[i][j][t*a[i][j]%k]|=dp[i-1][j][t]||dp[i][j-1][t];
	}

	int ans=0;
	for(int i=0;i<k;i++) if(dp[n][m][i]) ans++; cout<<ans<<endl;
	for(int i=0;i<k;i++) if(dp[n][m][i]) cout<<i<<' ';
	return 0;
}
/*
dp[i][j][t]表示走到(i,j)时模数能不能为t 
*/

P1474 [USACO2.3] Money System / [USACO07OCT]Cow Cash G

https://www.luogu.com.cn/problem/P1474

博客写一半忘记保存了……又得重写一遍

完全背包板子题。设 \(dp(i,j)\) 表示用 \(1 \sim i\) 种货币面值 \(a_1 \sim a_i\) 恰好组合出 \(k\) 块钱的方案数,状态转移方程也很显然:

\[dp(i,j)_{1 \le i \le n}=\sum_{j=a_i}^m dp(i-1,j-a_i) \]

发现这个 \(i\) 每一次都是从上一个转移过来的,所以可以用滚动数组优化,直接把第一维干掉就好了。还要注意的是这个题不开 long long 见祖宗。

https://www.luogu.com.cn/record/230900279

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=30;
constexpr int M=10007;
int a[N],n,m;
ll dp[M];
int main()
{
//	freopen("neuvillette.in","r",stdin);
//	freopen("neuvillette.out","w",stdout);
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i];
	dp[0]=1;
	for(int i=1;i<=n;i++)
		for(int j=a[i];j<=m;j++)
			dp[j]+=dp[j-a[i]];
	cout<<dp[m];
	return 0;
}
/*
完全背包求方案数 
dp[j]表示恰好凑出j元的方案数 
*/

拔河比赛

题面 题目描述 一个学校举行拔河比赛,所有的人被分成了两组,每个人必须(且只能够)在其中的一组,要求两个组的人数相差不能超过1,且两个组内的所有人体重加起来尽可能地接近。(即两组人体重之和的差的绝对值尽可能小)

输入格式 输入数据的第 \(1\) 行是一个 \(n\),表示参加拔河比赛的总人数,\(n \le 100\),接下来的 \(n\) 行表示第 \(1\) 到第 \(n\) 个人的体重 \(w_i\),每个人的体重都是整数 \(1 \le w_i \le 450)\)

输出格式 输出数据应该包含两个整数:分别是两个组的所有人的体重和,用一个空格隔开。注意如果这两个数不相等,则请把小的放在前面输出。

样例

【样例输入】
3
100
90
200
【样例输出】
190 200

来源:poj 2756

洛谷好像有一个题目 U291756 拔河问题1,但是我怀疑这个数据有问题,在学校 OJ 和 POJ 原题能过的:https://vjudge.net/solution/62884531

发现所有人的体重之和 \(\sum w_i\) 只有 \(450 \times 100=45000\) 比较小,所以还是可以沿用 P2049 魔术棋子“判定有没有可能”的思想。设 \(dp(i,j,k)\) 表示从前 \(i\) 个人中选择 \(j\) 个人,这些人的体重之和有没有可能等于 \(k\)。然后就有以下的状态转移:

\[dp(i,j,k)=dp(i,j,k) \operatorname{or} dp(i,j-1,k-a_i) \]

\(i\) 只从 \(i-1\) 转移,所以可以省略第一维。

最后在从 \(0 \sim s\) 中找出一个绝对值之差最小的答案就可以了。

#include<iostream>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int N=307;
const int K=450*N;
int n,a[N],s=0;
bool dp[N][K];
int abs(int x)
{
	return x>0?x:-x;
}
int main()
{
//	freopen("data.in","r",stdin);
//	freopen("data.out","w",stdout);
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
		s+=a[i];
	}
	dp[0][0]=1;
	for(int i=1;i<=n;i++)
		for(int j=(n+1)/2;j>=1;j--)
			for(int k=s;k>=a[i];k--)
				dp[j][k]|=dp[j-1][k-a[i]];
	
	int mn=998244353,ansa=-1,ansb=-1;
	for(int k=0;k<=s;k++)
	{
		if(dp[n/2][k])
		{
			int a=k,b=s-k;
			if(abs(a-b)<mn)
			{
				mn=abs(a-b);
				ansa=min(a,b);
				ansb=max(a,b);
			}
		}
		if(n%2==1&&dp[(n+1)/2][k])
		{
			int a=k,b=s-k;
			if(abs(a-b)<mn)
			{
				mn=abs(a-b);
				ansa=min(a,b);
				ansb=max(a,b);
			}
		}
	}
	cout<<ansa<<' '<<ansb;
	return 0;
}
/*
dp[i][j][k]表示从前i个人中选出j个人,这些人的体重之和有没有可能等于k 
使用滚动数组省略第一维 
*/ 

P3146 [USACO16OPEN] 248 G

https://www.luogu.com.cn/problem/P3146

\(dp(l,r)\) 表示合并区间 \([l,r]\) 能获得的得分的最大值。然后直接套上区间 DP 的模板,枚举一个分割点 \(k \in [l,r]\) 如果可以合并(\(dp(l,k)=dp(k+1,r)\))的话那就直接合并 \(dp(l,r) \gets \max\{dp(l,r),dp(l,k)+1\}\),否则就是负无穷。

注:状态不能设计成“区间 \([l,r]\) 能获得的得分的最大值”,否则你就会获得 49pts 的好成绩,因为有可能出现区间不能合并但是有最大分值,直接拿区间的最大分值合并上了,比如 \(7,1,7\) 直接合并成 \(8\) 了。

https://www.luogu.com.cn/record/230949724


P2340 [USACO03FALL] Cow Exhibition G

https://www.luogu.com.cn/problem/P2340

状态应该很容易设计,\(dp(i,x)\) 表示前 \(i\) 头牛智商为 \(x\) 时情商的最大值,转移方程就直接参考 0-1 背包就好了,还能用滚动数组干掉第一维。但主要是下标为负数的情况怎么办?

可以设计一个指针 int *dp=_dp+K,其中 _dp 为原来的 DP 数组,\(K\)\(1000 \times n=4000000\),利用指针的特性,我们就可以直接操作负数下标了。这就是魔术技巧

https://www.luogu.com.cn/record/230990282

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=409;
constexpr int K=400000;  //在DP数组里面将智商向右平移K个单位 
int _dp[2*K+9],n,a[N],b[N];
int *dp;
int main()
{
//	freopen("neuvillette.in","r",stdin);
//	freopen("neuvillette.out","w",stdout);
	dp=_dp+K;  //平移 
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i]>>b[i];
	memset(_dp,-0x3f,sizeof _dp);
	dp[0]=0;
	for(int i=1;i<=n;i++)
	{
		if(a[i]<0) 
			for(int j=-K;j-a[i]<=K;j++) dp[j]=max(dp[j],dp[j-a[i]]+b[i]);
		else
			for(int j=K;j-a[i]>=-K;j--) dp[j]=max(dp[j],dp[j-a[i]]+b[i]);
	}
	int ans=0;
	for(int i=0;i<=K;i++)
	{
		if(dp[i]>=0)ans=max(ans,i+dp[i]);
//		cerr<<i<<' '<<i-n*K<<' '<<dp[n][i]<<'\n'; 
	}
	cout<<ans;
	return 0;
}
/*
dp[i][x]表示前i头牛智商为x时情商的最大值 
第i头牛可以由第i-1头牛转移过来 
滚动数组优化掉第一维 
*/

P3985 不开心的金明

https://www.luogu.com.cn/problem/P3985

没时间写了,明天再写吧。

https://www.luogu.com.cn/record/230997497

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
constexpr int N=107;
int minv=INT_MAX;
int v[N],p[N];
int dp[N*3][N];
int main()
{
//	freopen("neuvillette.in","r",stdin);
//	freopen("neuvillette.out","w",stdout);
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		cin>>v[i]>>p[i];
		minv=min(minv,v[i]);
	}
	for(int i=1;i<=n;i++) v[i]-=minv;
	for(int i=1;i<=n;i++)
	{
		for(int j=3*N-1;j>=v[i];j--) 
		for(int cnt=n;cnt>0;cnt--) dp[j][cnt]=max(dp[j][cnt],dp[j-v[i]][cnt-1]+p[i]);
	}
	int ans=0;
	for(int i=1;i<=n;i++)
		for(int j=0;j<=3*n;j++)
			for(int cnt=0;cnt<=n;cnt++)
				if(j+cnt*minv<=m) ans=max(ans,dp[j][cnt]);
	cout<<ans;
	return 0;
}
/*
把原来的v减去minv之后得到一个新的价值v' 
设dp[j][cnt]表示花费j元购买cnt件物品的最大重要度
花费的真正的价值是j+cnt*minv 
*/
posted @ 2025-08-14 21:20  wwwidk1234  阅读(12)  评论(0)    收藏  举报