2026年9月短-DP专题训练
2026年9月短-DP专题训练
7-1 [USACO16DEC] Team Building
把两个人的奶牛分别按分数从小到大排序。令第一个人的奶牛数组为 \(a\),第二个人的为 \(b\)。
令 \(f[i][j][k]\) 表示分别在 John 的前 \(i\) 只奶牛和 Paul 的前 \(j\) 只奶牛中选 \(k\) 只且满足题目要求的方案数。
对于 \(a_i>b_j\) 的情况,可以从 \(f[i-1][j-1][k-1]\) 转移;别的情况都只能 \(k\) 保持不变地转移,这个转移比较像二维前缀和,不能简单地 \(f[i][j][k]=f[i-1][j][k]+f[i][j-1][k]\),需要减去重复计数的 \(f[i-1][j-1][k]\)。
转移最外层是 \(k\)。模数是 \(10^9+9\)。\(k=0\) 的时候要特殊处理,比如把整个二维前缀和赋为 \(1\)。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN=1e3+50,P=1e9+9;
int N,M,K;
int a[MAXN],b[MAXN];
int f[MAXN][MAXN][11];
int main()
{
cin>>N>>M>>K;
for(int i=1;i<=N;i++)
{
cin>>a[i];
}
for(int i=1;i<=M;i++)
{
cin>>b[i];
}
sort(a+1,a+N+1);
sort(b+1,b+M+1);
f[0][0][0]=0;
for(int i=0;i<=N;i++)
{
for(int j=0;j<=M;j++)
{
f[i][j][0]=1;
}
}
for(int k=1;k<=K;k++)
{
for(int i=1;i<=N;i++)
{
for(int j=1;j<=M;j++)
{
if(a[i]>b[j])
{
f[i][j][k]=(f[i][j][k]+f[i-1][j-1][k-1])%P;
}
f[i][j][k]=(1ll*f[i][j][k]+f[i-1][j][k]+f[i][j-1][k]-f[i-1][j-1][k])%P;
}
}
}
cout<<(f[N][M][K]%P+P)%P;
}
7-2 [ICPC 2017 WF] Posterize
题意比较难读明白。
预处理把每一个区间的所有颜色变成同一个颜色的最小误差,令为 \(g[l][r]\),\(l\) 和 \(r\) 是指输入的第 \(l\) 和第 \(r\) 个颜色。
令 \(f[i][j]\) 表示前 \(i\) 个不同颜色值合并为 \(j\) 个的最小误差,枚举 \(k\),则第 \([k+1,i]\) 个颜色变成同一个颜色。
\(f[i][j]=\min_{k=0}^{i-1}(f[k][j-1]+g[k+1][i])\)。
点击查看代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=300;
const ll INF=1e18;
int N,K;
int R[MAXN];
ll P[MAXN];
ll g[MAXN][MAXN],f[MAXN][MAXN];
ll Sqr(ll x)
{
return x*x;
}
int main()
{
cin>>N>>K;
for(int i=1;i<=N;i++)
{
cin>>R[i]>>P[i];
}
for(int l=1;l<=N;l++)
{
for(int r=l;r<=N;r++)
{
g[l][r]=INF;
}
}
for(int i=0;i<=255;i++)
{
for(int l=1;l<=N;l++)
{
ll Now=0;
for(int r=l;r<=N;r++)
{
Now+=P[r]*Sqr(R[r]-i);
g[l][r]=min(g[l][r],Now);
}
}
}
for(int i=0;i<=N;i++)
{
for(int j=0;j<=K;j++)
{
f[i][j]=INF;
}
}
f[0][0]=0;
for(int i=1;i<=N;i++)
{
for(int j=1;j<=K;j++)
{
f[i][j]=INF;
for(int k=0;k<=i-1;k++)
{
f[i][j]=min(f[i][j],f[k][j-1]+g[k+1][i]);
}
}
}
cout<<f[N][K];
}
7-3 [Lydsy2017年4月月赛]序列操作
最大得分是一个经典的贪心题,从左到右能操作就操作。
最小得分考虑最后的情况一定是某种 \(0\) 是单独出现或者成对出现的,且非 \(0\) 一定是单独出现的。
令 \(f[i]\) 表示前 \(i\) 位不存在连续的两个非 \(0\) 数字且第 \(i\) 位是非 \(0\) 数字的最小操作数。
考虑上一个非 \(0\) 数字的位置,一定只可能是 \(i-2\) 或 \(i-3\),更早的话就不好了。中间的数字都需要被按平,操作量至少为中间数字的较大值。
\(f[i]=\min(f[i-2],f[i-3]+\max(a[i-2],a[i-1]))\)。
初值 \(f[1]=0,f[2]=a[1]\)。答案为 \(\min(f[N],f[N-1]+a[N])\)。
点击查看代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e5+50;
int N;
ll a[MAXN];
ll f[MAXN];
void Solve()
{
cin>>N;
for(int i=1;i<=N;i++)
{
cin>>a[i];
}
f[0]=0;
f[1]=0;
f[2]=a[1];
for(int i=3;i<=N;i++)
{
f[i]=min(f[i-2]+a[i-1],f[i-3]+max(a[i-1],a[i-2]));
}
ll Min=min(f[N],f[N-1]+a[N]);
ll Max=0;
for(int i=1;i<N;i++)
{
ll Now=min(a[i],a[i+1]);
Max+=Now;
a[i]-=Now;
a[i+1]-=Now;
}
cout<<Min<<' '<<Max<<'\n';
}
int main()
{
ios::sync_with_stdio(false);
int T;
cin>>T;
while(T--)
{
Solve();
}
}
7-4 [Nwerc2015]Debugging调试
题意比较难读明白。
一次运行可以把整个程序分割为很多段,且知道 bug 在其中的哪个段。printf 的位置为分割点。
由于并不知道 bug 在哪,所以每次分割的时候尽可能使得每一段尽可能平均。
令 \(f[i]\) 表示长度为 \(i\) 的程序需要的最小时间,枚举 printf 的数量,然后分割成若干长度相等的小段(可能最后一段不足,但不影响答案,因为答案是最劣情况),递归处理。
关于复杂度:可以看做调和级数,\(O(n\log n)\)。写法记忆化搜索远优于递推。
点击查看代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e5+50;
int N;
ll a[MAXN];
ll f[MAXN];
void Solve()
{
cin>>N;
for(int i=1;i<=N;i++)
{
cin>>a[i];
}
f[0]=0;
f[1]=0;
f[2]=a[1];
for(int i=3;i<=N;i++)
{
f[i]=min(f[i-2]+a[i-1],f[i-3]+max(a[i-1],a[i-2]));
}
ll Min=min(f[N],f[N-1]+a[N]);
ll Max=0;
for(int i=1;i<N;i++)
{
ll Now=min(a[i],a[i+1]);
Max+=Now;
a[i]-=Now;
a[i+1]-=Now;
}
cout<<Min<<' '<<Max<<'\n';
}
int main()
{
ios::sync_with_stdio(false);
int T;
cin>>T;
while(T--)
{
Solve();
}
}
7-5 三维积木
这里把“列”定义为同一层前面会挡住后面的为在一列。钦定一个颜色为红色,另外两个颜色本质相同。最后答案枚举钦定的哪个颜色当红色,将答案乘三个不同的 \(\binom{G+B}{G}\) 再加起来就是真答案。
考虑一列的情况。
需要考虑这一列每一行的高度,最高高度,以及用的数量
\(f[i][j][k][x][y]\):前 \(i\) 行,第 \(i\) 行高度为 \(j\),最高高度为 \(k\),用了 \(x\) 个红色,\(y\) 个别的颜色。
\(j= 0\) 时候从前一行转移:
\(f[i][0][k][x][y]=\sum_{j=0}^{k}f[i-1][j][k][x][y]\)。
\(j\neq 0\) 时枚举当前放哪一种颜色,如果放的是红色,且 \(j=k\) 可以让最高高度变高,否则不行:
\(j=k\) 时 \(f[i][j][k][x][y]=f[i][j-1][k-1][x-1][y]+f[i][j-1][k][x-1][y]+f[i][j-1][k][x][y-1]\)。
否则:\(f[i][j][k][x][y]=f[i][j-1][k][x-1][y]+f[i][j-1][k][x][y-1]\)。
令 \(h[x][y]\) 表示一列放 \(x\) 个红色 \(y\) 个其它颜色的方案数。
令 \(g[i][x][y]\) 表示前 \(i\) 列用掉 \(x\) 个红色 \(y\) 个其它颜色的方案数,枚举当前列用了 \(x'\) 个红色 \(y'\) 个其它颜色,\(g[i][x][y]=\sum_{x'=0}^{x}\sum_{y'=0}^{y}g[i-1][x-x'][y-y']\times h[x'][y']\)。
点击查看代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=26,P=1e9+7;
int R,G,B,N;
ll f[MAXN][MAXN][MAXN][MAXN][MAXN*2],g[MAXN][MAXN][MAXN*2],h[MAXN][MAXN*2];
ll C[100][100];
int main()
{
cin>>R>>G>>B>>N;
int Cnt=R+G+B;
int Sum=Cnt,Max=max(R,max(G,B)),Min=min(R,min(G,B));
Sum-=Min;
C[0][0]=1;
for(int i=1;i<=Cnt;i++)
{
C[i][0]=1;
for(int j=1;j<=Cnt;j++)
{
C[i][j]=(C[i-1][j]+C[i-1][j-1])%P;
}
}
f[0][0][0][0][0]=1;
for(int i=1;i<=N;i++)
{
for(int k=0;k<=Max;k++)
{
for(int x=k;x<=Max;x++)
{
for(int y=0;y<=Sum;y++)
{
for(int j=0;j<=k;j++)
{
f[i][0][k][x][y]=(f[i][0][k][x][y]+f[i-1][j][k][x][y])%P;
}
for(int j=1;j<k;j++)
{
if(x)
f[i][j][k][x][y]=(f[i][j][k][x][y]+f[i][j-1][k][x-1][y])%P;
if(y)
f[i][j][k][x][y]=(f[i][j][k][x][y]+f[i][j-1][k][x][y-1])%P;
}
if(x)
{
f[i][k][k][x][y]=(f[i][k][k][x][y]+f[i][k-1][k-1][x-1][y])%P;
f[i][k][k][x][y]=(f[i][k][k][x][y]+f[i][k-1][k][x-1][y])%P;
}
if(y)
{
f[i][k][k][x][y]=(f[i][k][k][x][y]+f[i][k-1][k][x][y-1])%P;
}
}
}
}
}
for(int x=0;x<=Max;x++)
{
for(int y=0;y<=Sum;y++)
{
for(int k=0;k<=x;k++)
{
for(int j=0;j<=k;j++)
{
h[x][y]=(h[x][y]+f[N][j][k][x][y])%P;
}
}
}
}
g[0][0][0]=1;
for(int i=1;i<=N;i++)
{
for(int x=0;x<=Max;x++)
{
for(int y=0;y<=Sum;y++)
{
for(int _x=0;_x<=x;_x++)
{
for(int _y=0;_y<=y;_y++)
{
g[i][x][y]=(g[i][x][y]+g[i-1][x-_x][y-_y]*h[_x][_y])%P;
}
}
}
}
}
ll ans=0;
ans=(ans+g[N][R][G+B]*C[G+B][G])%P;
ans=(ans+g[N][G][R+B]*C[R+B][R])%P;
ans=(ans+g[N][B][R+G]*C[R+G][R])%P;
cout<<ans;
}
7-6 Minecraft的世界末日
跟 [SCOI2010] 股票交易 一模一样(除了输入格式)。而且题面比较烂,很多歧义(因为照搬股票交易但是改了名词导致很不符合常理)。Hydro 的这道题时限设太小。不建议做这道题。考虑做 [SCOI2010] 股票交易。
令 \(f[i][j]\) 表示前 \(i\) 天手里持股数为 \(j\) 的最多赚钱数(可能为负数)。
由于有交易时间限制,所以 \(f[i]\) 需要从 \(f[i-W-1]\) 转移过来。\(f[i-W-1]\) 是包含了整个前缀的状态的。
最基础的转移:第 \(i\) 天不做事。
\(f[i][j]=f[i-W-1][j]\)。
第 \(i\) 天买股票:
\(f[i][j]=\max_{k=\max(0,j-AS_i)}^{j-1}f[i-W-1][k]-AP_i\times(j-k)=-AP_i \times j +\max_{k=\max(0,j-AS_i)}^{j-1}f[i-W-1][k]+k\times AP_i\)
令 \(g[k]=f[i-W-1][k]+k\times AP_i\),则 \(f[i][j]=-AP_i\times j+\max_{k=\max(0,j-AS_i)}^{j-1}g[k]\)。
发现后面求 \(\max\) 是滑动窗口问题。用单调队列优化 \(dp\) 即可。复杂度 \(O(T \times MaxP)\)。
卖股票同理。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN=3e3+50,INF=1e9;
int T,MaxP,W;
struct Day
{
int AP,BP,AS,BS;
}d[MAXN];
int f[MAXN][MAXN];
int main()
{
ios::sync_with_stdio(false);
cin>>T>>MaxP>>W;
for(int i=1;i<=T;i++)
{
cin>>d[i].AP>>d[i].BP>>d[i].AS>>d[i].BS;
}
for(int i=0;i<=T+1;i++)
{
for(int j=0;j<=MaxP+1;j++)
{
f[i][j]=-INF;
}
}
int Now=0;
f[0][0]=0;
deque<int>q;
for(int i=1;i<=T;i++)
{
for(int j=0;j<=d[i].AS;j++)
{
f[i][j]=-j*d[i].AP;
}
for(int j=0;j<=MaxP;j++)
{
f[i][j]=max(f[i][j],f[i-1][j]);
}
if(i>W)
{
q.clear();
for(int j=0;j<=MaxP;j++)
{
while(!q.empty()&&q.front()<j-d[i].AS)
{
q.pop_front();
}
while(!q.empty()&&f[i-W-1][q.back()]+q.back()*d[i].AP<=f[i-W-1][j]+j*d[i].AP)
{
q.pop_back();
}
q.push_back(j);
f[i][j]=max(f[i][j],f[i-W-1][q.front()]+(q.front()-j)*d[i].AP);
}
q.clear();
for(int j=MaxP;j>=0;j--)
{
while(!q.empty()&&q.front()>j+d[i].BS)
{
q.pop_front();
}
while(!q.empty()&&f[i-W-1][q.back()]+q.back()*d[i].BP<=f[i-W-1][j]+j*d[i].BP)
{
q.pop_back();
}
q.push_back(j);
f[i][j]=max(f[i][j],f[i-W-1][q.front()]+(q.front()-j)*d[i].BP);
}
}
}
int ans=0;
for(int i=0;i<=MaxP;i++)
{
ans=max(ans,f[T][i]);
}
cout<<ans<<'\n';
}
7-7 宝石镶嵌(BZOJ 4976)
没有找到公开的可以交题的地方。
给一个长度为 \(n\) 的数组 \(w\),选择其中 \(k\) 个位置删掉,求剩下的值的按位或最大是多少。\(0 \leq n,w_i \leq 10^5,0 \leq k \leq 100\)。
考虑二进制每一位都找一个数字使得这一位为 \(1\),由于 \(w_i\) 至多 \(17\) 位,所以至多只需要保留 \(17\) 个数字就能使得答案为理论最大值 \(OR_{i=1}^{n}w_i\)。
也就是只需要考虑 \(n-k< 17\) 的情况,此时 \(n < 117\)。
考虑背包,令 \(f[i][S]\) 表示前 \(i\) 位或出来为 \(S\) 最少需要保留多少个数字。考虑填表法转移。
对于第 \(i\) 个位置,枚举 \(f[i-1][S]\) 可以转移到 \(f[i][S|w_i]\) 这个位置,花费 \(1\) 的代价。也可以不花代价 \(f[i][S]=f[i-1][S]\)。所以这里初始化的时候要把 \(f[0][S]\) 设置为正无穷,且把 \(f[0][0]\) 设置为 \(0\)。
最后找到最大的一个数字能被 \(n-k\) 个数字以内表示出来。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN=1e6+50;
int N,K;
int w[MAXN];
int f[MAXN];
int main()
{
cin>>N>>K;
int Or=0;
for(int i=1;i<=N;i++)
{
cin>>w[i];
Or|=w[i];
}
if(N-K>=17)
{
cout<<Or;
return 0;
}
for(int i=1;i<=Or;i++)
{
f[i]=N+1;
}
Or=0;
for(int i=1;i<=N;i++)
{
for(int j=Or;j>=0;j--)
{
f[j|w[i]]=min(f[j|w[i]],f[j]+1);
}
Or|=w[i];
}
int ans=0;
for(int i=Or;i>=0;i--)
{
if(f[i]<=N-K)
{
ans=i;
break;
}
}
cout<<ans;
}
7-8 密码破译(BZOJ 5043)
依旧找不到公开的可以交题的地方。
有一个人把一个长度为 \(n\) 的数组 \(a\) 的所有值同时异或了一个值 \(k\)。并把异或后的数组 \(b\) 给你。再给你 \(a\) 的和 \(m\),求最小的 \(k\) 或报告无解。
\(1 \leq n \leq 10^5,0 \leq b_i,m < 2^{60}\)。
考虑暴搜剪枝。
从高位到低位,先搜 \(k\) 这一位为 \(0\) 有没有解,无解再搜 \(k\) 这一位为 \(1\)。
剪枝策略就是考虑从最高位到当前位 \(1\) 的位的 \(m-\sum a >\) 后续位能凑出的最大值,就不继续搜了。这里的 \(a\) 只考虑搜过的位,因为没搜过的位不能确定。
复杂度粗略分析只要连续错了几位就不可能搜下去了,所以复杂度不会太大。
中间变量可能会爆 long long。
点击查看代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e5+50;
const ll INF=1e18;
int N;
ll M;
ll b[MAXN];
int Cnt[65];
ll ans=INF;
__int128 Max[65];
__int128 L[65],R[65];
void dfs(int Id,__int128 Now,__int128 K)
{
if(Now>M||M-Now>Max[Id])
return;
if(Id==-1)
{
if(Now==M)
{
ans=K;
}
return;
}
dfs(Id-1,Now+L[Id],K);
if(ans==INF)
dfs(Id-1,Now+R[Id],K+(1ll<<Id));
}
int main()
{
cin>>N>>M;
for(int i=1;i<=N;i++)
{
cin>>b[i];
for(int j=0;j<=60;j++)
{
Cnt[j]+=((b[i]>>j)&1);
}
}
for(int j=0;j<=60;j++)
{
L[j]=(__int128)Cnt[j]*(1ll<<j);
R[j]=(__int128)(N-Cnt[j])*(1ll<<j);
if(j==0)
{
Max[j]=max(L[j],R[j]);
}
else
{
Max[j]=Max[j-1]+max(L[j],R[j]);
}
}
dfs(60,0,0);
if(ans!=INF)
{
cout<<ans;
}
else
cout<<-1;
}
本文来自博客园,作者:0htoAi,转载请注明原文链接:https://www.cnblogs.com/0htoAi/p/22924790

浙公网安备 33010602011771号