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\)。所以有以下的转移方程:
然后把图里面每一条边反着建立然后跑一遍拓扑排序就可以了。
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\) 中选择任意个不重复的区间所吃到的最多的牧草数量,然后显然地有一下状态转移方程:
这个时间复杂度是 \(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}\) 表示逻辑或运算):
最后枚举一下 \(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\) 块钱的方案数,状态转移方程也很显然:
发现这个 \(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\)。然后就有以下的状态转移:
\(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
*/

浙公网安备 33010602011771号