2023ACM暑期集训 DAY 5&6
模拟赛 2 题解
目前进度——线性dp、背包问题,区间
好题
1038 [NOIP2008]传纸条
标签
高维 DP
思路
- 难点在于如何保证从 \((1,1)\) 到 \((n,m)\) 与从 \((n,m)\) 到 \((1,1)\) 的两条路径不存在一点重合。发现,回来的路径与前往的路径没有本质区别,故可按找两条从 \((1,1)\) 到 \((n,m)\) 的不交叉路径的好心程度之和最大对待。故可设 \(dp_{i,j,k,l}\) 表示第一条路径从 \((1,1)\) 走到 \((i,j)\),第二条路径从 \((1,1)\) 走到 \((k,l)\) 好心程度和的最大值。只需要保证任意状态 \(dp_{i,j,k,l}\) 只有当 \(i\ne k\) 且 \(j\ne l\),才能被转移得到,便可使得 \(dp_{n,m,n,m}\) 是由两条不交叉路径得到的。
- 时间复杂度为 \(\mathcal O(n^2m^2)\)。
代码
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=55;
int dp[maxn][maxn][maxn][maxn];
int n,m,a[maxn][maxn];
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
scanf("%d",&a[i][j]);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
for(int k=1;k<=n;k++)
{
for(int l=1;l<=m;l++)
if(i!=k && j!=l)
dp[i][j][k][l]=max(dp[i][j][k][l],
a[i][j]+a[k][l]+max(dp[i-1][j][k-1][l],
max(dp[i][j-1][k][l-1],
max(dp[i][j-1][k-1][l],dp[i-1][j][k][l-1]))));
}
}
}
dp[n][m][n][m]=max(dp[n][m][n][m],
max(dp[n-1][m][n-1][m],
max(dp[n][m-1][n][m-1],
max(dp[n][m-1][n-1][m],dp[n-1][m][n][m-1]))));
printf("%d",dp[n][m][n][m]);
return 0;
}
类似题目——1039 [NOIP2000]方格取数
1041 [NOIP2010]乌龟棋
标签
高维 DP
思路
- 描述状态的重点在于知道目前状态的卡牌使用情况。故设 \(dp_{i,j,k,l}\) 表示用了 \(i\) 张 \(1\)、\(j\) 张 \(2\)、\(k\) 张 \(3\)、\(l\) 张 \(4\) 所能得到的最大分数。
- 时间复杂度为 \(\mathcal O(\prod\limits_{i=1}^{4}num_{i})\),\(num_i\) 表示 \(i\) 的个数。
代码
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int maxb=45,maxn=355;
int n,m,dp[maxb][maxb][maxb][maxb],book[5];
int a[maxn];
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
int x;
for(int i=1;i<=m;i++)
scanf("%d",&x),book[x]++;
for(int i=0;i<=book[1];i++)
for(int j=0;j<=book[2];j++)
for(int k=0;k<=book[3];k++)
for(int l=0;l<=book[4];l++)
{
if(i>=1) dp[i][j][k][l]=max(dp[i][j][k][l],
dp[i-1][j][k][l]);
if(j>=1) dp[i][j][k][l]=max(dp[i][j][k][l],
dp[i][j-1][k][l]);
if(k>=1) dp[i][j][k][l]=max(dp[i][j][k][l],
dp[i][j][k-1][l]);
if(l>=1) dp[i][j][k][l]=max(dp[i][j][k][l],
dp[i][j][k][l-1]);
dp[i][j][k][l]+=a[1+i+2*j+3*k+4*l];
}
printf("%d",dp[book[1]][book[2]][book[3]][book[4]]);
return 0;
}
1042 To the Max
标签 1
前缀和
思路
- 统计二维前缀和,暴力枚举矩形的左上角与右下加。
- 时间复杂度为 \(\mathcal O(n^4)\)。
标签 2
DP
前缀和
思路 2
- 枚举矩阵的上底所在的行,以及下底所在的行。注意到矩形的列是连续的。故令 \(dp_k\) 表示上底为 \(i\) 行,下底为 \(j\) 行,以 \(k\) 列为右列结尾的矩形的最大数字和,则 \(dp_k\) 可由 \(dp_{k-1}\) 转移而来。故预处理列前缀和即可。
- 时间复杂度为 \(\mathcal O(n^3)\)。
代码 2
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=105,INF=1e7;
int n,a[maxn][maxn],ans=-INF,dp[maxn];
//int sum[maxn][maxn];
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
scanf("%d",&a[i][j]);
//sum[i][j]=-sum[i-1][j-1]+sum[i-1][j]+sum[i][j-1]+a[i][j];
}
}
int col[maxn];
for(int i=1;i<=n;i++)
{
memset(col,0,sizeof(col));
for(int j=i;j<=n;j++)
{
for(int k=1;k<=n;k++)
col[k]+=a[j][k],
dp[k]=-INF; dp[0]=-INF;
for(int k=1;k<=n;k++)
{
dp[k]=max(col[k],dp[k-1]+col[k]);
ans=max(ans,dp[k]);
}
}
}
/*for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
for(int k=i;k<=n;k++)
{
for(int l=j;l<=n;l++)
ans=max(ans,sum[k][l]-sum[k][j-1]-sum[i-1][l]+sum[i-1][j-1]);
}
}
}*/
printf("%d",ans);
return 0;
}
1043 [ZJOI2007]棋盘制作
标签
DP 悬线法
贪心
思路
- DP 悬线法板子题。
代码
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e3+5;
int h[maxn][maxn],l[maxn][maxn],r[maxn][maxn];
int n,m,ans,ansi,a[maxn][maxn];
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
scanf("%d",&a[i][j]);
}
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
h[i][j]=max(1,(a[i-1][j]==a[i][j]?0:h[i-1][j]+1)),
l[i][j]=max(1,(a[i][j-1]==a[i][j]?0:l[i][j-1]+1));
for(int i=1;i<=n;i++)
for(int j=m;j>=1;j--)
r[i][j]=max(1,(a[i][j]==a[i][j+1]?0:r[i][j+1]+1));
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(i>1 && a[i][j]!=a[i-1][j])
l[i][j]=min(l[i][j],l[i-1][j]),
r[i][j]=min(r[i][j],r[i-1][j]);
int c=r[i][j]+l[i][j]-1;
ans=max(c*h[i][j],ans);
ansi=max(ansi,min(c,h[i][j])*min(c,h[i][j]));
}
}
cout<<ansi<<"\n"<<ans;
return 0;
}
1034 [USACO 2009 Dec G]Video Game Troubles
标签
状态分离讨论 DP
思路
- 状态分离讨论,即不令所有状态在代码的同一位置完成讨论,分离出可以单独讨论而不影响状态转移正确性的状态。
- 设 \(dp_{i,j}\) 表示在前 \(i\) 个游戏机里选,有 \(j\) 块钱所能达到的最大价值和。可先讨论,仅购买游戏机的情况,状态转移方程为 \(dp_{i,j}=dp_{i-1}{j-p_i},j-p_i\ge 0\);在此基础上讨论,在属于游戏机 \(i\) 的前 \(k\) 个游戏中选,有 \(j\) 块钱所能达到的最大价值和,状态转移方程为,\(dp_{i,j}=\max(dp_{i,j},dp_{i,j-pi_k}+v_k),j-pi_k\ge 0\),注意这里是对买游戏的0/1背包,并使用了滚动数组。然后,再讨论不买游戏机 \(i\) 的情况,状态转移方程为 \(dp_{i,j}=\max(dp_{i,j},dp_{i-1}{j})\)。
- 时间复杂度为 \(\mathcal O(nmk)\)。
代码
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=55;
const int maxv=1e5+100;
int p[maxn],n,m;
int dp[2][maxv];
vector<pair<int,int> > son[maxn];
int main()
{
scanf("%d%d",&n,&m);
int x,v,pi;
for(int i=1;i<=n;i++)
{
scanf("%d%d",&p[i],&x);
for(int j=1;j<=x;j++)
{
scanf("%d%d",&pi,&v);
son[i].push_back(make_pair(pi,v));
}
}
for(int i=1;i<=n;i++)
{
for(int j=m;j>=p[i];j--)
dp[1][j]=max(dp[1][j],dp[0][j-p[i]]);
for(int k=0;k<son[i].size();k++)
{
pi=son[i][k].first,v=son[i][k].second;
for(int j=m;j>=pi+p[i];j--)
dp[1][j]=max(dp[1][j],dp[1][j-pi]+v);
}
for(int j=1;j<=m;j++)
dp[1][j]=max(dp[1][j],dp[0][j]),
dp[0][j]=dp[1][j],dp[1][j]=0;
}
printf("%d",dp[0][m]);
return 0;
}
1048 [NOIP2007]守望者的逃离
标签
状态分离讨论 DP
贪心
思路
- 容易发现,不使用闪烁魔术而以 \(17m/s\) 的向前行走的方式与魔力值无关,故可单独进行状态转移。
- 考虑,依靠闪烁魔术进行转移的方式。根据贪心可知,魔力值够就用闪烁魔术,不够了停下来添加,够了继续用。以上述原则进行状态转移。
- 再将普通行走的状态单独加上即可。
代码
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int maxt=300005;
int dp[maxt],m,s,t;
int main()
{
scanf("%d%d%d",&m,&s,&t);
for(int i=1;i<=t;i++)
{
if(m>=10) dp[i]=dp[i-1]+60,m-=10;
else dp[i]=dp[i-1],m+=4;
}
for(int i=1;i<=t;i++)
if(dp[i]<dp[i-1]+17) dp[i]=dp[i-1]+17;
if(dp[t]>=s) printf("Yes\n%d",lower_bound(dp+1,dp+t+1,s)-dp);
else printf("No\n%d",dp[t]);
return 0;
}
1044 [SCOI2005]最大子矩阵
标签
状态分离讨论 DP
思路
- 首先对矩阵进行转置,易得,转置不改变答案。
- 若矩阵只有 \(1\) 行,则设 \(dp_{i,j}\) 表示在前 \(i\) 列选出 \(j\) 个矩阵所能达到的最大值,则 \(dp_{i,j}=\max(dp_{i-1,j},dp_{p-1,j-1}+sum_{i}-sum_{p-1})\)。
- 若矩阵有两行,受到只有 \(1\) 行的操作的启发,设 \(dp_{i,j,k}\) 表示在第一行前 \(i\) 列,第二行前 \(j\) 列,选出 \(k\) 个矩阵的最大价值。则状态转移有以下情况:第二行已遍历到第 \(j\) 列,第一行的 \(i\) 参与/未参与第 \(k\) 个子矩阵的选取;第一行以遍历到第 \(i\) 列,第二行的 \(j\) 参与/未参与第 \(k\) 个子矩阵的选取;第一行与第二行分别没有遍历到 \(i\) 与 \(j\),\(a_{1,i}\) 与 \(a_{1,j}\) 同时参与第 \(k\) 个子矩阵的选取,此时要求 \(i=j\)。
代码
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=105;
const int INF=1e8;
int n,m,dp[maxn][maxn],dpi[maxn][maxn][maxn];
int sum[maxn],sumi[maxn],a[maxn][maxn],k;
int main()
{
scanf("%d%d%d",&m,&n,&k);
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++)
scanf("%d",&a[j][i]);
if(n==1)
{
for(int i=1;i<=m;i++)
sum[i]=sum[i-1]+a[1][i];
for(int i=1;i<=m;i++)
for(int j=1;j<=k;j++)
dp[i][j]=-INF;
for(int i=1;i<=m;i++)
{
for(int j=1;j<=k;j++)
{
dp[i][j]=dp[i-1][j];
for(int p=1;p<=i;p++)
dp[i][j]=max(dp[i][j],
dp[p-1][j-1]+(sum[i]-sum[p-1]));
}
}
printf("%d",dp[m][k]);
}
else
{
for(int i=1;i<=m;i++)
sum[i]=sum[i-1]+a[1][i],
sumi[i]=sumi[i-1]+a[2][i];
for(int i=1;i<=m;i++)
for(int j=1;j<=m;j++)
for(int ki=1;ki<=k;ki++)
dpi[i][j][ki]=-INF;
for(int i=1;i<=m;i++)
{
for(int j=1;j<=m;j++)
{
for(int ki=1;ki<=k;ki++)
{
dpi[i][j][ki]=max(dpi[i-1][j][ki],dpi[i][j-1][ki]);
for(int p=1;p<=i;p++)
dpi[i][j][ki]=max(dpi[i][j][ki],
dpi[p-1][j][ki-1]+(sum[i]-sum[p-1]));
for(int p=1;p<=j;p++)
dpi[i][j][ki]=max(dpi[i][j][ki],
dpi[i][p-1][ki-1]+(sumi[j]-sumi[p-1]));
if(i==j)
for(int p=1;p<=i;p++)
dpi[i][j][ki]=max(dpi[i][j][ki],
dpi[p-1][p-1][ki-1]+(sum[i]-sum[p-1])+
+(sumi[i]-sumi[p-1]));
}
}
}
printf("%d",dpi[m][m][k]);
}
return 0;
}
类似题目——1038 [NOIP2008]传纸条
1049 [NOIP2018]货币系统
标签
统计类 DP
思维
思路
- 突破点是发现两个货币系统等价的充分必要条件是两个货币系统可以相互表出。故寻找等价的简便货币系统,即为从原货币系统中提取出若干个数,使得这些数表出任意原货币系统中的任意数。
- 显然,小数无法被大数表出,能表出的大数一定是小数的线性组合。故对原货币系统中的数进行由小到大排序,用 \(dp_{i,j}\) 表示前 \(i-1\) 个数是否能表出 \(j\) 进行统计,不能被标出的则加入新系统。
代码
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=105;
int t,n,a[maxn],p,ans;
int dp[26000];
int main()
{
scanf("%d",&t);
while(t--)
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
sort(a+1,a+n+1);
memset(dp,0,sizeof(dp));
ans=0;
dp[0]=1;
for(int i=1;i<=n;i++)
{
if(!dp[a[i]]) ans++;
for(int j=0;j<=25000;j++)
if(j-a[i]>=0) dp[j]|=dp[j-a[i]];
}
printf("%d\n",ans);
}
return 0;
}
类似的题——1051 [HAOI2012]音量调节、1050 [USACO 1050 2009 Oct G]Bessie's Weight Problem
1046 [USACO 2016 Ope P]262144
标签
倍增 DP
统计类 DP
思路
- 区间 DP可实现操作,但时间复杂度 \(\mathcal O(n^2)\) 不可取。
- 对于状态的转移难点在于:不知道某个数是从哪个区间合并得到的,以及某个数的具体值是多少。故设状态 \(dp_{i,j}\) 表示以 \(j\) 为左端点,以 \(dp_{i,j}\) 为右端点的区间合并后值为 \(i\)。则有初始化 \(dp_{a_i,i}=i\),并当 \(dp_{i,j}=0\) 时有转移方程 \(dp_{i,j}=[dp_{i-1,dp_{i-1,j}+1}!=0]\)。因为转移方程类似倍增,故叫倍增 DP。
代码
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=262149;
int dp[60][maxn],n,a,ans;
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a);
dp[a][i]=i;
ans=max(ans,a);
}
for(int i=1;i<=58;i++)
{
for(int j=1;j<=n;j++)
if(!dp[i][j]&&dp[i-1][j])
dp[i][j]=dp[i-1][dp[i-1][j]+1],
dp[i][j]?ans=i:0;
}
cout<<ans;
}
1040 [NOIP2005]过河
标签
路径压缩
思路
- 因为青蛙的路径有有周期 \(\operatorname{lcm}(S,T)\),又 \(S*T\ge \operatorname{lcm}(S,T)\),故若两个位置上临近的石头的距离大于 \(2*ST\),则将距离改为 \(2*ST\) 即可。特殊地,需要特殊处理 \(S==T\) 的情形。
代码
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=105;
int n,l,s,t,m[maxn],a[maxn],flag[100005];
int dp[100005];
int main()
{
scanf("%d",&l);
scanf("%d%d%d",&s,&t,&n);
for(int i=1;i<=n;i++)
scanf("%d",&m[i]);
sort(m+1,m+n+1);
if(s==t)
{
int ans=0;
for(int i=1;i<=n;i++)
ans+=m[i]%s?0:1;
printf("%d",ans);
}
else
{
for(int i=1;i<=n;i++)
{
int d=m[i]-m[i-1];
if(d>2*s*t) a[i]=a[i-1]+2*s*t;
else a[i]=a[i-1]+d;
flag[a[i]]=1;
}
memset(dp,0x7f7f7f7f,sizeof(dp));
dp[0]=0;
for(int i=1;i<=a[n]+2*s*t;i++)
{
for(int j=s;j<=t;j++)
if(i-j>=0) dp[i]=min(dp[i],dp[i-j]+flag[i]);
}
int ans=0x7f7f7f7f;
for(int i=a[n];i<=a[n]+2*s*t;i++)
ans=min(ans,dp[i]);
printf("%d",ans);
}
return 0;
}
1036 凸多边形的划分
标签
数学
区间 DP
提醒
不要误以为只有从一个顶点发出 \(N-2\) 个线段才能将 \(N\) 边形分成 \(N\) 个不相交的三角形。
代码
点击查看代码
n=eval(input())
a=[int(e) for e in input().split()]
a=a+a
dp=[[10**40 for _ in range(n)]for _ in range(n)]
for i in range(n):
if i+2<n:
dp[i][i+2]=a[i]*a[i+1]*a[i+2]
if i+1<n:
dp[i][i+1]=0
if i<n:
dp[i][i]=0
def cal(x,y,z):
if x==y or x==z or y==z:
return 0
return a[x]*a[y]*a[z]
for li in range(4,n+1):
for l in range(n):
r=l+li-1
if r>=n:
break
for p in range(l+1,r):
dp[l][r]=min(dp[l][r],\
dp[l][p]+dp[p][r]+cal(l,r,p))
print(dp[0][n-1])