DP 专项练习
[USACO23OPEN] Pareidolia S
对于这种题,两种思路,一种是直接 ,一种是考虑每个 bessie
产生的贡献。
显然直接考虑 bessie
产生的贡献难以解决 bbessie
的情况,所以考虑 。
设 表示以 开头的字符串的总贡献,那么显然有 ,考虑如何转移。
用 bessie
来划分,对于 ,找到往后 的位置,记作 ,那么这个 bessie
就会在后边产生 的贡献,同时再加上 即可。即:
[USACO09OPEN] Grazing2 S
考虑直接 ,设 表示前 个奶牛,最后一个放在 这个牛棚,的最小移动距离。
转移是显然的,直接对于每个 枚举 以及与 的距离不超过 的即可。
但是这样时间复杂度是 ,显然不行。
先将所有牛按照初始位置排序,对于第 头牛,其最前的位置显然位于 ,最后的位置位于 ,也就是说对于 只需要枚举这些位置进行转移即可,事件复杂度为 ,注意转移过程要用滚动数组优化。
const int N=1e6+10; int n,s,a[N]; LL f[N],g[N]; int main() { cin>>n>>s; int d=(s-1)/(n-1); for(int i=1;i<=n;i++) cin>>a[i]; sort(a+1,a+1+n); memset(f,0x3f,sizeof(f)); memset(g,0x3f,sizeof(g)); f[1]=g[1]=abs(a[1]-1); for(int i=2;i<=n;i++) { for(int j=1+(i-1)*d;j<=s-(n-i)*d;j++) { int p=j-d; f[j]=g[p]+abs(a[i]-j); if(p-1>=1+(i-2)*d) f[j]=min(f[j],g[p-1]+abs(a[i]-j)); } for(int j=1+(i-1)*d;j<=s-(n-i)*d;j++) { g[j]=f[j]; } } cout<<f[s]; return 0; }
[USACO16OPEN] 262144 P
对于这道题的弱化版,直接设 表示 这段期间合并出的最大数字,然后 转移即可。
但是这道题,不要说 ,就是连 都过不了,这是我们就要转化状态设计。
原来我们将合并出来的数字当作答案,位置当作状态,不妨反过来,将合并出来的数字当作状态,位置当作答案,具体而言,设 表示以 开头,合并出数字 的右端点,显然右端点只有一个,所以不需熬考虑那么多。
考虑转移,由于两个相同的数字合并成一个大 的数字,所以有如下式子:
考虑最大的数字会合并出什么,由于两个数字可以变大 ,所以最大的数字就是 最大是 ,所以处理到 即可。
const int N=3e5,M=61; int n,a[N]; int f[N][M]; int main() { cin>>n; for(int i=1;i<=n;i++) { int x; cin>>x; f[i][x]=i; } for(int i=1;i<=60;i++) { for(int j=1;j<=n;j++) { if(f[j][i]) continue; if(!f[j][i-1]) continue; f[j][i]=f[f[j][i-1]+1][i-1]; } } for(int i=60;i>=1;i--) { for(int j=1;j<=n;j++) { if(f[j][i]) { cout<<i; return 0; } } } return 0; }
[USACO04DEC] Cleaning Shifts S
直接设 表示前 分钟最少需要几头奶牛,也就是最少选几头奶牛才可以覆盖。
对于朴素算法,对于每个 ,我们需要找到 且 最小的 ,毕竟贪心的考虑,肯定是覆盖的越多越好。
首先将数组按照 从小到大排序,那么对于一个 ,满足第一个条件的就是一个后缀,那么我们对每个后缀求一个 的 ,如果这个 仍然不符合第 个条件,说明无法转移,输出无解;否则直接用这个转移即可。
const int N=3e4,M=1e6+10; int t,n; int minn[N],f[M]; struct node { int s,e; }a[N]; int cmp(node a,node b) { return a.e<b.e; } int main() { cin>>n>>t; memset(minn,0x3f,sizeof(minn)); for(int i=1;i<=n;i++) { a[i].s=read(); a[i].e=read(); } sort(a+1,a+1+n,cmp); for(int i=n;i>=1;i--) minn[i]=min(minn[i+1],a[i].s); int p=1; memset(f,0x3f,sizeof(f)); f[0]=0; for(int i=1;i<=t;i++) { while(a[p].e<i&&p<=n) p++; if(a[p].e<i||minn[p]>i) { cout<<-1; return 0; } f[i]=min(f[i],f[minn[p]-1]+1); } cout<<f[t]; return 0; }
[USACO20OPEN] Exercise G
首先将题目描述进行抽象,不难发现,对于题目给出的 ,就是下面这张图:
不难发现,对于左边这个环,需要转 次才能回到原来的形状,右边的环,需要转 次才 ,所以总的转数就是取最小公倍数,即 。
那么我们便可以将问题抽象成如下:将 分解为若干个数字之和,对这些数字取 ,问一共有多少种不同的 个数。
如果直接对怎么拆分进行考虑,显然是不妥的,因为若现在拆分出来的数字和之前的有公因数,那么贡献很难计算,但如果我们单独对素数进行考虑,显然一个素数与其他数字(除非自己,这种情况后面再说)是没有公因数的,这样会方便转移,并且任何一个数皆可以表示为素数的乘积。
设 表示前 个数字,最大的素数是 的总拆分数,由于求的是种类数之和,所以我们直接钦定从小到大进行拆分。对于相同的数字,我们在一次转移中直接以指数幂的方式放进去,与之前的不重复即可。
为了转移方便,在维护一个数组 表示前 个数字,最大的素数不超过 的总数。这个数组在 数组计算完一次后前缀和维护即可。
有如下转移:
const int N=1e4+10,M=1e4,L=1300; int n,MOD; int vis[N],m,pr[N]; LL f[N][L],g[N][L]; void shai() { pr[++m]=1; for(int i=2;i<=M;i++) { if(!vis[i]) pr[++m]=i; for(int j=2;j<=m&&i*pr[j]<=M;j++) { vis[pr[j]*i]=1; if(i%pr[j]==0) break; } } } int main() { cin>>n>>MOD; shai(); f[0][0]=1; for(int i=0;i<=m;i++) g[0][i]=1; for(int i=1;i<=n;i++){ for(int j=1;j<=m&&pr[j]<=i;j++) { if(pr[j]==1) { f[i][j]=1; } else { for(int k=pr[j];k<=i;k*=pr[j]) { f[i][j]=(f[i][j]+g[i-k][j-1]*(LL)k%MOD)%MOD; } } } for(int j=1;j<=m;j++) { g[i][j]=(g[i][j-1]+f[i][j])%MOD; } } LL ans=0; for(int i=1;i<=m;i++) { ans=(ans+f[n][i])%MOD; } cout<<ans; return 0; }
[HNOI2004] 敲砖块
不难发现直接 并不容易,不妨先找一找合法方案的规律。
可以发现,若选择了 ,那么 往下的全部三角形都要选上。但是这样仍然存在问题,选择的两个点可能重合,也就是三角形区域有可能重合,造成贡献难以计算,也就是下图,但如果考虑仅仅保留三角形顶上的轮廓,应该说是不难转移的。
可以发现,轮廓线的走向要么直接向下,要么取右上,我们只需要按照转移写出方程即可。
const int N=52; int n,m; LL a[N][N],f[N][N][N*N],sum[N][N]; int main() { cin>>n>>m; for(int i=1;i<=n;i++) { for(int j=1;j<=n-i+1;j++) { cin>>a[i][j]; } } memset(f,-0x3f,sizeof(f)); for(int j=1;j<=n;j++) { f[1][j][1]=a[1][j]; for(int i=1;i<=n-j+1;i++) sum[j][i]=sum[j][i-1]+a[i][j]; for(int z=1;z<j;z++) {//需要处理中间没有轮廓线的情况。 for(int k=2;k<=m;k++) { f[1][j][k]=max(f[1][j][k],f[1][z][k-1]+a[1][j]); } } for(int i=1;i<=n-j+1;i++) { for(int k=1;k<=m;k++) { f[i][j][k]=max(f[i][j][k],f[i-1][j][k-1]+a[i][j]); if(k-i<0||j==1) continue; for(int z=min(i+1,n-j+2);z>=2;z--) { f[i][j][k]=max(f[i][j][k],f[z][j-1][k-i]+sum[j][i]); } } } } LL ans=0; for(int i=1;i<=n;i++) { for(int j=0;j<=m;j++) ans=max(ans,f[1][i][j]); } cout<<ans; return 0; }
[JSOI2007] 重要的城市
对于 的数据结构,加上在最短路的背景下,基本可以确定是用类似 的方式进行 。
的本质就是逐个加点,然后将这个点作为中转点取更新其他点,那么我们设想,如果一个点加入后,有两点之间的最短路马上被更新,一定可以说明这个点在当前时刻属于两点之间的必须经过点;那么如果更新的值与之前的相等,那么这两点之间没有必须经过的点,不设置中转城市。
最后只需要将所有点对的必过点拿出来,排序去重输出即可。
const int N=210; int n,m; LL f[N][N],ans[N][N],cnt,b[N*N]; int main() { cin>>n>>m; memset(f,0x3f,sizeof(f)); for(int i=1;i<=n;i++) f[i][i]=0; for(int i=1;i<=m;i++) { int a,b; LL c; cin>>a>>b>>c; f[a][b]=min(f[a][b],c); f[b][a]=min(f[b][a],c); } for(int k=1;k<=n;k++) { for(int i=1;i<=n;i++) { for(int j=1;j<=n;j++){ if(f[i][j]>f[i][k]+f[k][j]) { f[i][j]=f[i][k]+f[k][j]; if(k!=i&&k!=j) ans[i][j]=k; } else if(f[i][j]==f[i][k]+f[k][j]) { if(k!=i&&k!=j) ans[i][j]=0; } } } } for(int i=1;i<=n;i++) { for(int j=1;j<=n;j++) { if(ans[i][j]) b[++cnt]=ans[i][j]; } } sort(b+1,b+1+cnt); cnt=unique(b+1,b+1+cnt)-b-1; if(!cnt) cout<<"No important cities."; else { for(int i=1;i<=cnt;i++) cout<<b[i]<<" "; } return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效