DP 专项练习
[USACO23OPEN] Pareidolia S
对于这种题,两种思路,一种是直接 \(dp\),一种是考虑每个 bessie
产生的贡献。
显然直接考虑 bessie
产生的贡献难以解决 bbessie
的情况,所以考虑 \(dp\)。
设 \(f_{i}\) 表示以 \(i\) 开头的字符串的总贡献,那么显然有 \(ans=\sum_{i=1}^{len}f_i\),考虑如何转移。
用 bessie
来划分,对于 \(i\),找到往后 \(e\) 的位置,记作 \(j\),那么这个 bessie
就会在后边产生 \(len-j+1\) 的贡献,同时再加上 \(f_{j+1}\) 即可。即:
[USACO09OPEN] Grazing2 S
考虑直接 \(dp\),设 \(dp_{i,j}\) 表示前 \(i\) 个奶牛,最后一个放在 \(j\) 这个牛棚,的最小移动距离。
转移是显然的,直接对于每个 \(i\) 枚举 \(j\) 以及与 \(j\) 的距离不超过 \(d\) 的即可。
但是这样时间复杂度是 \(\rm O(NS)\),显然不行。
先将所有牛按照初始位置排序,对于第 \(i\) 头牛,其最前的位置显然位于 \(1+(i-1)\times d\),最后的位置位于 \(s-(n-i)\times d\),也就是说对于 \(i\) 只需要枚举这些位置进行转移即可,事件复杂度为 \(O(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
对于这道题的弱化版,直接设 \(f_{i,j}\) 表示 \(i\sim j\) 这段期间合并出的最大数字,然后 \(\rm O(n^3)\) 转移即可。
但是这道题,不要说 \(O(n^3)\),就是连 \(O(n^2)\) 都过不了,这是我们就要转化状态设计。
原来我们将合并出来的数字当作答案,位置当作状态,不妨反过来,将合并出来的数字当作状态,位置当作答案,具体而言,设 \(f_{i,j}\) 表示以 \(i\) 开头,合并出数字 \(j\) 的右端点,显然右端点只有一个,所以不需熬考虑那么多。
考虑转移,由于两个相同的数字合并成一个大 \(1\) 的数字,所以有如下式子:
考虑最大的数字会合并出什么,由于两个数字可以变大 \(1\),所以最大的数字就是 \(40+\log N\) 最大是 \(60\),所以处理到 \(60\) 即可。
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
直接设 \(f_i\) 表示前 \(i\) 分钟最少需要几头奶牛,也就是最少选几头奶牛才可以覆盖。
对于朴素算法,对于每个 \(i\),我们需要找到 \(E_j>=i\) 且 \(S_j\) 最小的 \(j\),毕竟贪心的考虑,肯定是覆盖的越多越好。
首先将数组按照 \(E\) 从小到大排序,那么对于一个 \(i\),满足第一个条件的就是一个后缀,那么我们对每个后缀求一个 \(S\) 的 \(minn\),如果这个 \(minn\) 仍然不符合第 \(2\) 个条件,说明无法转移,输出无解;否则直接用这个转移即可。
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
首先将题目描述进行抽象,不难发现,对于题目给出的 \(A=(2,3,1,5,4)\),就是下面这张图:
不难发现,对于左边这个环,需要转 \(3\) 次才能回到原来的形状,右边的环,需要转 \(2\) 次才 \(ok\),所以总的转数就是取最小公倍数,即 \(6\)。
那么我们便可以将问题抽象成如下:将 \(n\) 分解为若干个数字之和,对这些数字取 \(\rm lcm\),问一共有多少种不同的 \(\rm lcm\) 个数。
如果直接对怎么拆分进行考虑,显然是不妥的,因为若现在拆分出来的数字和之前的有公因数,那么贡献很难计算,但如果我们单独对素数进行考虑,显然一个素数与其他数字(除非自己,这种情况后面再说)是没有公因数的,这样会方便转移,并且任何一个数皆可以表示为素数的乘积。
设 \(f_{i,j}\) 表示前 \(i\) 个数字,最大的素数是 \(j\) 的总拆分数,由于求的是种类数之和,所以我们直接钦定从小到大进行拆分。对于相同的数字,我们在一次转移中直接以指数幂的方式放进去,与之前的不重复即可。
为了转移方便,在维护一个数组 \(g_{i,j}\) 表示前 \(i\) 个数字,最大的素数不超过 \(j\) 的总数。这个数组在 \(f\) 数组计算完一次后前缀和维护即可。
有如下转移:
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] 敲砖块
不难发现直接 \(dp\) 并不容易,不妨先找一找合法方案的规律。
可以发现,若选择了 \((i,j)\),那么 \((i,j)\) 往下的全部三角形都要选上。但是这样仍然存在问题,选择的两个点可能重合,也就是三角形区域有可能重合,造成贡献难以计算,也就是下图,但如果考虑仅仅保留三角形顶上的轮廓,应该说是不难转移的。
可以发现,轮廓线的走向要么直接向下,要么取右上,我们只需要按照转移写出方程即可。
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] 重要的城市
对于 \(n\le 200\) 的数据结构,加上在最短路的背景下,基本可以确定是用类似 \(Floyed\) 的方式进行 \(dp\)。
\(Floyed\) 的本质就是逐个加点,然后将这个点作为中转点取更新其他点,那么我们设想,如果一个点加入后,有两点之间的最短路马上被更新,一定可以说明这个点在当前时刻属于两点之间的必须经过点;那么如果更新的值与之前的相等,那么这两点之间没有必须经过的点,不设置中转城市。
最后只需要将所有点对的必过点拿出来,排序去重输出即可。
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;
}