上讲习题
AcWing 292
这个题目是互相攻击类的题目,很容易想到每一排的状态然后判断两排是否能放在一起。但是不难发现,本题行与行之间的限制有两排,则在 f f f数组中要存下最两排选择的是什么状态才可以确定是否可以新加某一行。设 f i , j , k f_{i,j,k} fi,j,k表示确定完了前 i i i行,倒数两行的状态分别是 j , k j,k j,k的最大放置个数。设 c n t u cnt_u cntu为选择状态 u u u能够多放的炮车数量,则有状态转移方程 f i , k , u = max ( f i , k , u , f i − 1 , j , k + c n t u ) f_{i,k,u}=\max(f_{i,k,u},f_{i-1,j,k}+cnt_u) fi,k,u=max(fi,k,u,fi−1,j,k+cntu)。然而我们发现,本题的数据范围较大,这样肯定会超过空间限制。我们又发现,第 i i i行的状态只依赖于第 i − 1 i-1 i−1行,则可以考虑滚动数组,只用存下第 i i i行和第 i − 1 i-1 i−1行的状态即可。
#include<bits/stdc++.h>
using namespace std;
const int NN=(1<<10)+4;
int g[104],cnt[NN],f[2][NN][NN];
vector<int>state;
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
char s[14];
scanf("%s",s);
for(int j=0;j<m;j++)
g[i]+=(s[j]=='H')<<j;
}
for(int i=0;i<1<<m;i++)
if(!((i>>1&i)||(i>>2&i)))
{
state.push_back(i);
int t=i;
while(t)
{
cnt[state.size()-1]++;
t-=t&-t;
}
}
for(int i=1;i<=n;i++)
for(int j=0;j<state.size();j++)
for(int k=0;k<state.size();k++)
for(int u=0;u<state.size();u++)
{
int a=state[j],b=state[k],c=state[u];
if(!((a&b)||(a&c)||(b&c)||(g[i-1]&b)||(g[i]&c)))
f[i&1][k][u]=max(f[i&1][k][u],f[i-1&1][j][k]+cnt[u]);
}
int ans=0;
for(int i=0;i<state.size();i++)
for(int j=0;j<state.size();j++)
ans=max(ans,f[n&1][i][j]);
printf("%d",ans);
return 0;
}
AcWing 524
不难发现,每个抛物线都可以覆盖一些点,我们可以统计每种抛物线覆盖了哪些点。但是抛物线的个数是无限的,但是只有选择过了某些点的抛物线才是有价值的,所以可以枚举过哪些点。抛物线的表达式 y = a x 2 + b x + c y=ax^2+bx+c y=ax2+bx+c,本题中 c = 0 c=0 c=0,要求覆盖某集合点的抛物线,则给定 x x x,求 a , b a,b a,b,两个未知数用两个式子即可,则两个点唯一确定一个抛物线,判断是否满足题目要求即可。每个点一定有任意一个抛物线经过它,则枚举的两个点相同时可以有一个只覆盖它一个点的抛物线。解决了预处理,就是计算最优的覆盖了。设 f i , j f_{i,j} fi,j为处理了前 i i i个抛物线,已经覆盖了 j j j的最小代价。因为一定要覆盖到每一个点,则可以枚举最前面且没覆盖的点,然后枚举用哪一个覆盖它的抛物线。设 g g g为抛物线能覆盖的点集,最前面没有覆盖的点为 x x x,则有 f i + 1 , j ∣ g x , j = min ( f i , j + 1 , f i , j ∣ g x , j , f i + 1 , j ∣ g x , j ) f_{i+1,j|g_{x,j}}=\min(f_{i,j}+1,f_{i,j|g_{x,j}},f_{i+1,j|g_{x,j}}) fi+1,j∣gx,j=min(fi,j+1,fi,j∣gx,j,fi+1,j∣gx,j),发现 i + 1 i+1 i+1的状态只依赖于 i i i的状态,则可以省去 i i i的一维。最后,输出覆盖全部的最小代价即可。边界条件:只有一个点都不覆盖没有代价,其余都为正无穷。
#include<bits/stdc++.h>
using namespace std;
const int NN=1000004,MM=20;
int f[NN],g[MM][MM];
double x[MM],y[MM];
int cmp(double x,double y)
{
if(abs(y-x)<1e-6)
return 0;
if(x<y)
return -1;
return 1;
}
int main()
{
int t;
scanf("%d",&t);
while(t--)
{
int n;
scanf("%d%*d",&n);
for(int i=0;i<n;i++)
scanf("%lf%lf",&x[i],&y[i]);
memset(g,0,sizeof(g));
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
if(i!=j)
{
if(!cmp(x[i],x[j]))
continue;
double a=(y[i]/x[i]-y[j]/x[j])/(x[i]-x[j]),b=y[i]/x[i]-a*x[i];
if(cmp(a,0)>=0)
continue;
for(int k=0;k<n;k++)
g[i][j]+=!cmp(x[k]*x[k]*a+x[k]*b,y[k])<<k;
}
else
g[i][j]=1<<i;
memset(f,0x3f,sizeof(f));
f[0]=0;
for(int i=0;i+1<1<<n;i++)
{
int sum;
for(int j=0;j<n;j++)
if(!(i>>j&1))
{
sum=j;
break;
}
for(int j=0;j<n;j++)
f[i|g[sum][j]]=min(f[i|g[sum][j]],f[i]+1);
}
printf("%d\n",f[(1<<n)-1]);
}
}
概念
区间 d p dp dp,其实就是解决一个区间的一个最值的一种 d p dp dp。
方法
这种类型的 d p dp dp一般都是用区间的左右边界作为状态,然后从中间找到一个中转的位置,分成几个区间分别解决。这种 d p dp dp为了保证先算子问题,所以一般会把区间的长度放在最外层。当然,区间 d p dp dp用记忆化搜索会更简单。
例题
AcWing 1068
这个题因为是求一个区间合并的最值,而且两个连着的区间可以轻易合并成一个区间,所以可以用区间 d p dp dp。设 f i , j f_{i,j} fi,j为区间 [ i , j ] [i,j] [i,j]合并成一个数所需的最小代价。则可以将其分为两个区间分别合并成一个数,再将两个区间合并出的数合并成一个数,则 f i , j = min ( f i , k + f k + 1 , j + ∑ x = i j a x , i ≤ k ≤ j ) f_{i,j}=\min(f_{i,k}+f_{k+1,j}+\displaystyle\sum_{x=i}^ja_x,i\leq k\leq j) fi,j=min(fi,k+fk+1,j+x=i∑jax,i≤k≤j),但是本题的区间是环形的,所以可以再接一串到后面,这样所有情况就都考虑了。因为本题要求和,则可以前缀和来完成该操作。为了保证先算某个状态的子问题,则第一层循环按长度枚举。最大代价同理。
#include<bits/stdc++.h>
using namespace std;
const int NN=204;
int a[NN],f1[NN][NN],f2[NN][NN],x[NN];
int main()
{
int n,ans1=1e9,ans2=0;
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&x[i]);
a[i]=a[i-1]+x[i];
}
for(int i=n+1;i<=n*2;i++)
a[i]=a[i-1]+x[i-n];
for(int s=2;s<=n;s++)
for(int i=1;i<=2*n-s+1;i++)
{
int j=i+s-1;
f1[i][j]=1e9;
for(int k=i;k<j;k++)
{
f1[i][j]=min(f1[i][j],f1[i][k]+f1[k+1][j]+a[j]-a[i-1]);
f2[i][j]=max(f2[i][j],f2[i][k]+f2[k+1][j]+a[j]-a[i-1]);
}
}
for(int i=1;i<=n;i++)
{
ans1=min(ans1,f1[i][i+n-1]);
ans2=max(ans2,f2[i][i+n-1]);
}
printf("%d\n%d",ans1,ans2);
return 0;
}
AcWing 479
这个题目发现,因为给出的是中序遍历,则选中点 x x x为该子树的根,左边为左子树,右边为右子树,相当于分成了左右两部分又分别计算。我们惊奇地发现:这和区间 d p dp dp很像啊!而且要求的最值分值也十分符合区间 d p dp dp的性质,就是左区间的分值乘右区间的分值加上自己的分值。那么要求方案怎么办?从哪里转移的就相当于以哪里为根,记录一下从哪里转移的然后递归计算即可。某个点可能没有左子树或右子树,按题目要求没有子树则该子树分值为 1 1 1,则 f i + 1 , i = 1 f_{i+1,i}=1 fi+1,i=1即可。最后,状态转移方程不必多讲, f i , j = max ( f i , k − 1 × f k + 1 , j + a k , i ≤ k ≤ j ) f_{i,j}=\max(f_{i,k-1}\times f_{k+1,j}+a_k,i\leq k\leq j) fi,j=max(fi,k−1×fk+1,j+ak,i≤k≤j)
#include<bits/stdc++.h>
using namespace std;
const int NN=34;
int f[NN][NN],pre[NN][NN];
void print(int l,int r)
{
if(l>r)
return;
printf("%d ",pre[l][r]);
print(l,pre[l][r]-1);
print(pre[l][r]+1,r);
}
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&f[i][i]);
pre[i][i]=i;
f[i+1][i]=1;
}
f[1][0]=1;
for(int s=2;s<=n;s++)
for(int i=1;i+s-1<=n;i++)
{
int j=i+s-1;
for(int k=i;k<=j;k++)
if(f[i][j]<f[i][k-1]*f[k+1][j]+f[k][k])
{
f[i][j]=f[i][k-1]*f[k+1][j]+f[k][k];
pre[i][j]=k;
}
}
printf("%d\n",f[1][n]);
print(1,n);
return 0;
}
AcWing 1069
本题看着毫无头绪,但是很容易想出来,每一次一定会划分出一个三角形。但是即使这样,也不知道该以哪几个顶点划分。怎么办呢?因为如果要把多边形划分成三角形,则多边形的每一条边都会在划分的某一个三角形上。于是对于整个多边形,我们可以先将包含 1 , n 1,n 1,n这条棱的三角形划分出来。但是还缺少一个顶点 k k k,则可以枚举剩下的顶点 k k k。不难发现,如果用一个 k k k割开,则会分割出来两个互不干扰的图形,第一个图形中点的编号为 1... k 1...k 1...k,第二个图形中点的编号为 k . . . n k...n k...n。将其一般化,设 f i , j f_{i,j} fi,j为点的编号为 i . . . j i...j i...j的多边形划分的最小代价,则有状态转移方程 f i , j = a i × a j × a k + f i , k + f k , j f_{i,j}=a_i\times a_j\times a_k+f_{i,k}+f_{k,j} fi,j=ai×aj×ak+fi,k+fk,j。因为本题数据范围极大,需要高精度计算。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int NN=54,MM=34;
int w[NN];
ll f[NN][NN][MM];
void add(ll a[],ll b[])
{
int t=0;
for(int i=0;i<MM;i++)
{
t+=a[i]+b[i];
a[i]=t%10;
t/=10;
}
}
void mul(ll a[],ll b)
{
ll t=0;
for(int i=0;i<MM;i++)
{
t+=a[i]*b;
a[i]=t%10;
t/=10;
}
}
int cmp(ll a[],ll b[])
{
for(int i=MM-1;i>=0;i--)
if(a[i]>b[i])
return 1;
else if(a[i]<b[i])
return -1;
return 0;
}
void print(ll a[])
{
int k=MM-1;
while(k&&!a[k])
k--;
while(k>=0)
printf("%d",a[k--]);
}
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&w[i]);
ll temp[MM];
for(int len=3;len<=n;len++)
for(int l=1;l+len-1<=n;l++)
{
int r=l+len-1;
f[l][r][MM-1]=1;
for(int k=l+1;k<r;k++)
{
memset(temp,0,sizeof(temp));
temp[0]=w[l];
mul(temp,w[k]);
mul(temp,w[r]);
add(temp,f[l][k]);
add(temp,f[k][r]);
if(cmp(f[l][r],temp)>0)
memcpy(f[l][r],temp,sizeof(temp));
}
}
print(f[1][n]);
return 0;
}
习题
AcWing 320
AcWing 321
解析和代码在下一篇博客——树形 d p dp dp给出