· 不依赖任何形式的外部变量
· 答案以返回值而非参数形式存在
· 对于相同参数返回值相同
递归实现转移,因此是反向的
把这道题的dp状态和方程写出来
根据他们写出dfs函数
添加记忆化数组
写出这道题的暴搜程序(最好是dfs)
将这个dfs改成"无需外部变量"的dfs
添加记忆化数组
具体过程详见下文 P2758 编辑距离
可以空间优化的根本原因:
第i个状态仅能转移到i-1个
即当一层状态更新完毕,就不会影响其余的状态
如果正向枚举
不满足此性质
记忆化搜索
#include
#include
#include
#include
typedef unsigned long long ull;
int const maxn=5010,maxm=5010,inf=0x1f1f1f1f;
int n,t,val[maxn],cost[maxn],ans[maxn][maxn];
int dfs(int vleft,int step)
{
if(ans[step][vleft]!=-1)
return ans[step][vleft];
if(step>n)
return ans[step][vleft]=0;
int nput=-inf,put=-inf;
nput=dfs(vleft,step+1);
if(cost[step]<=vleft)
put=dfs(vleft-cost[step],step+1)+val[step];
return ans[step][vleft]=std::max(nput,put);
}
int main()
{
memset(ans,-1,sizeof(ans));
scanf("%d%d",&t,&n);
for(int i=1;i<=n;i++)
scanf("%d%d",&cost[i],&val[i]);
printf("%d",dfs(t,1));
return 0;
}
递推
#include
#include
#include
#include
typedef unsigned long long ull;
int const maxn=50100,maxm=50100,inf=0x1f1f1f1f;
int n,t,val[maxn],cost[maxn],ans[maxn];
int main()
{
scanf("%d%d",&t,&n);
for(register int i=1;i<=n;i++)
scanf("%d%d",&cost[i],&val[i]);
for(register int i=1;i<=n;i++)
for(register int j=t;j>=cost[i];j--)
ans[j]=std::max(ans[j],ans[j-cost[i]]+val[i]);
printf("%d",ans[t]);
return 0;
}
一开始读错了题,以为要刚好填满,就想把体积作为v,把问题转化成可行性背包
事实上该题可以把体积作为w,每当f[j]>W时,统计一下答案
#include
#include
int const maxn=10101,maxm=10101,inf=0x1f1f1f1f;
int W,n,V,cv[maxn],w[maxn],f[maxn],ans;
int main()
{
ans=-inf;
scanf("%d%d%d",&W,&n,&V);
for(int i=1;i<=n;i++)
scanf("%d%d",&w[i],&cv[i]);
for(int i=1;i<=n;i++)
for(int j=V;j>=cv[i];j--)
{
f[j]=std::max(f[j],f[j-cv[i]]+w[i]);
if(f[j]>=W)
ans=std::max(ans,V-j);
}
if(ans<0)
{
puts("Impossible");
return 0;
}
printf("%d",ans);
return 0;
}
模型竟然是可行性01背包求解方案数问题,完全没看出来…
题意其实是取某些数使他们的和等于集合内的某个数,每个数只能取一次,问有多少种方案,这样就很明显了
把集合内最大的数作为上限,背包必须填满
跑完背包只要把每个数对应的背包加起来即可
注意!要把自己相等的方案减去
#include
#include
#include
#include
int const maxn=501,maxm=501,inf=0x1f1f1f1f;
int t,n,f[1110],a[maxn],ans,mx;
int main()
{
scanf("%d",&t);
while(t--)
{
memset(f,0,sizeof(f));
ans=0;
mx=-1;
scanf("%d",&n);
f[0]=1;
for(int i=1;i<=n;i++)
scanf("%d",&a[i]),mx=std::max(mx,a[i]);
for(int i=1;i<=n;i++)
for(int j=mx;j>=a[i];j--)
f[j]+=f[j-a[i]];
for(int i=1;i<=n;i++)
ans+=f[a[i]];
printf("%d\n",ans-n);
}
return 0;
}
还算有意思的可行性01背包
然而这种水题我竟然没看出来
对于每一堆积木,都求一下可行性,用桶统计一下每个高度的可行性
当某个高度的的可行性达到n种,说明n堆积木都能凑出这个高度,作为一个可行解
从高到低枚举保证答案最优
没有可行解要特判
#include
#include
#include
int const maxn=1110,maxm=100110,inf=0x1f1f1f1f;
int f[maxm],cv[maxn];
int n,sum,mn=inf,cnt,ton[maxm];
int min(int x,int y,int z)
{
return std::min(std::min(x,y),z);
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
memset(f,0,sizeof(f));
f[0]=1;
cnt=0,sum=0;
for(int x;;)
{
scanf("%d",&x);
if(x==-1)
break;
cv[++cnt]=x;
sum+=x;
}
mn=std::min(mn,sum);
for(int j=1;j<=cnt;j++)
for(int k=sum;k>=cv[j];k--)
f[k]=std::max(f[k],f[k-cv[j]]);
for(int k=1;k<=sum;k++)
ton[k]+=f[k];
}
for(int k=mn;k>=0;k--)
if(ton[k]==n)
{
printf("%d",k);
return 0;
}
printf("0");
return 0;
}
完全背包统计可行性方案数问题
其实不是特别明白…
感性理解一下就是,选择某个面值,就能获得组成当前面值的方案数
因为选择某个数,方案数是不会改变的
#include
#include
#include
#include
typedef long long ll;
int const maxn=10110,maxm=10110,inf=0x1f1f1f1f;
int n,T;
ll f[maxn],cv[maxn];
int main()
{
scanf("%d%d",&n,&T);
for(int i=1;i<=n;i++)
scanf("%lld",cv+i);
f[0]=1;
for(int i=1;i<=n;i++)
for(int j=cv[i];j<=T;j++)
f[j]+=f[j-cv[i]];
printf("%lld",f[T]);
return 0;
}
比较显然的背包问题
把奶牛个数看成容积,耗时看做价值
问题转化为了可行性最小背包问题
由于同一奶牛个数可以重复选,比如可以每次只带一只奶牛所以是完全背包
#include
#include
#include
int const maxn=2511,maxm=2511;
int f[maxn],pre,n,m;
int main()
{
memset(f,0x1f,sizeof(f));
scanf("%d%d",&n,&m);
f[0]=-m,pre=m;
for(int w,i=1;i<=n;i++)
{
scanf("%d",&w),pre+=w;
for(int j=i;j<=n;j++)
f[j]=std::min(f[j],f[j-i]+pre+m);
}
printf("%d",f[n]);
return 0;
}
一开始没看到从1开始…
而且价值跟容积完全搞反了…
可行性完全背包
f[i]表示装到价值为i最少需要多少邮票
当f[j-w]>=sum时不能转移
#include
#include
#include
int const maxn=54,maxm=2001000,inf=0x1f1f1f1f;
int f[maxm];
int n,sum;
int main()
{
memset(f,0x1f,sizeof(f));
f[0]=0;
scanf("%d%d",&sum,&n);
for(int w,i=1;i<=n;i++)
{
scanf("%d",&w);
for(int j=w;j<=maxm;j++)
{
if(f[j-w]>=sum)
continue;
f[j]=std::min(f[j],f[j-w]+1);
}
}
for(int i=1;i<=maxm;i++)
if(f[i]==inf)
{
printf("%d",i-1);
return 0;
}
}
分组背包的可行性方案数问题
f[i][j]表示前i种的和为j的方案数
对于每一种,都跑一遍01背包
因为每个种都从上一种转移,所以每一种一定只能选一个
#include
#include
#include
int const maxn=10011,maxm=111,inf=0x1f1f1f1f;
int f[maxm][maxn],n,K,cv[maxm][maxm],maxx[maxm],minn[maxm],mx,mn;
int main()
{
scanf("%d%d",&n,&K);
for(int i=1;i<=n;i++)
{
maxx[i]=-1;
scanf("%d",&cv[i][0]);
for(int k=1;k<=cv[i][0];k++)
{
scanf("%d",&cv[i][k]);
maxx[i]=std::max(maxx[i],cv[i][k]);
}
mx+=maxx[i];
}
f[0][0]=1;
for(int i=1;i<=n;i++)
for(int j=1;j<=cv[i][0];j++)
for(int k=mx;k>=cv[i][j];k--)
f[i][k]+=f[i-1][k-cv[i][j]];
for(int k=1;k<=mx&&K;k++)
{
while(f[n][k]&&K)
{
f[n][k]--,K--;
printf("%d ",k);
}
}
return 0;
}
没想到区间dp也能用分组背包水…
最优性分组背包+路径记录
分为n组,每一组的物品是选1~m个物品,价值是收益
注意!!!分组背包的滚动仅限于最优解背包,不适用方案数背包!
价值应该是w[i][k]
#include
#include
#include
int const maxn=111,maxm=210,inf=0x1f1f1f1f;
int f[maxm],w[maxn][maxm],path[maxn][maxm];
int n,V;
int main()
{
scanf("%d%d",&n,&V);
for(int i=1;i<=n;i++)
for(int j=1;j<=V;j++)
scanf("%d",&w[i][j]);
for(int i=1;i<=n;i++)
for(int j=V;j>=1;j--)
for(int k=1;k<=j;k++)
{
int flag=0;
int lst=j-k;
if(f[j]
相当于有两个限制条件的背包
基本转移如下
for(int i=1;i<=n;i++)
for(int j=下限1;j<=限制1;j++)
for(int k=下限2;k<=限制2;k++)
f[j][k]=std::max(f[j][k],f[j-cv1[i]][k-cv2[i]);
裸的二维费用背包+输出路径
#include
#include
#include
int const maxn=111,maxm=210,inf=0x1f1f1f1f;
int f[maxm][maxm],cv1[maxm],cv2[maxn],w[maxn],path[maxm][maxm][maxn];
int n,V1,V2;
int main()
{
scanf("%d%d%d",&V1,&V2,&n);
for(int i=1;i<=n;i++)
scanf("%d%d%d",&cv1[i],&cv2[i],&w[i]);
for(int i=1;i<=n;i++)
for(int j=V1;j>=cv1[i];j--)
for(int k=V2;k>=cv2[i];k--)
{
int lst1=j-cv1[i],lst2=k-cv2[i];
if(f[j][k]
二维费用方案数背包
一开始试图用Y的积木的思路写,给他分成四组
结果这道题要求不超过四个,所以不能这么做
虽然这两者很像,但还是略有不同
分组背包的状态表示的是考虑完i组限制j能得到的答案
二维费用背包表示考虑完限制i后限制j能得得到的答案
(根据这道题的特殊性,我们需要把每个限制i得到的答案加起来)
#include
#include
using namespace std;
int f[5][32770],n,t;
int main()
{
f[0][0]=1,n=32768;
for(int i=1;i*i<=n;i++)
for(int j=i*i;j<=n;j++)
for(int k=1;k<=4;k++)
f[k][j]+=f[k-1][j-i*i];
scanf("%d",&t);
while(t--)
{
scanf("%d",&n);
int ans=0;
for(int k=1;k<=4;k++)
ans+=f[k][n];
printf("%d\n",ans);
}
return 0;
}
把上述所有背包给合起来即可
分组背包+多重背包+完全背包
#include
#include
#include
int const maxn=2112,inf=0x1f1f1f1f;
int n,V,f[maxn];
int min(int x,int y,int z)
{
return std::min(std::min(x,y),z);
}
void Pro(int a,int b)
{
for(int j=V;j>=0;j--)
for(int v=0;v<=j;v++)
{
int w=a*v*v-b*v;
f[j]=std::max(f[j],f[j-v]+w);
}
}
void Pzo(int v,int w)
{
for(int j=V;j>=v;j--)
f[j]=std::max(f[j],f[j-v]+w);
}
void Pcp(int v,int w)
{
for(int j=v;j<=V;j++)
f[j]=std::max(f[j],f[j-v]+w);
}
void Pmu(int v,int w,int nm)
{
if(v*nm>=V)
{
Pcp(v,w);
return;
}
int k=1;
while(k<=nm)
{
Pzo(v*k,w*k);
nm-=k;
k*=2;
}
Pzo(v*nm,w*nm);
}
int main()
{
scanf("%d%d",&n,&V);
for(int op,w,cv,num,i=1;i<=n;i++)
{
scanf("%d%d%d",&op,&w,&cv);
if(op==1)
Pro(w,cv);
else if(op==2)
{
scanf("%d",&num);
Pmu(cv,w,num);
}
else
Pcp(cv,w);
}
printf("%d",f[V]);
return 0;
}
#include
#include
#include
#include
typedef unsigned long long ull;
int const maxn=111,maxm=111,inf=0x1f1f1f1f;
int const dx[4]={0,0,1,-1};
int const dy[4]={1,-1,0,0};
int map[maxn][maxn],f[maxn][maxn],ans,n,m;
int cmp(int x,int y)
{
return map[x]
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
ai | 1 | 2 | 3 | 4 | 5 | 16 | 17 | 18 | 19 | 6 | 15 | 24 |
我们发现一个数x的上下左右有这样的关系,于是就可以转移了
x-m-1 | x-m | x-m+1 |
---|---|---|
x-1 | x | x+1 |
x+m-1 | x+m | x+m+1 |
#include
#include
#include
#include
typedef unsigned long long ull;
int const maxn=10110,maxm=10110,inf=0x1f1f1f1f;
int const dx[4]={0,0,1,-1};
int const dy[4]={1,-1,0,0};
int map[maxn],a[maxn],ans,f[maxn];
int cmp(int x,int y)
{
return map[x]
#include
#include
#include
#include
typedef unsigned long long ull;
int const maxn=51,maxm=51,inf=0x1f1f1f1f;
int const dx[4]={0,0,-1,-1};
int const dy[4]={-1,-1,0,0};
int const dxf[4]={-1,0,-1,0};
int const dyf[4]={0,-1,0,-1};
int n,m,map[maxn][maxn],f[maxn][maxn][maxn][maxn];
int main()
{
scanf("%d",&n);
for(int x,y,z;;)
{
scanf("%d%d%d",&x,&y,&z);
if(!x&&!y&&!z)
break;
map[x][y]=z;
}
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
for(int k=1;k<=n;k++)
{
int l=i+j-k;
if(l<=0)
continue;
for(int p=0;p<4;p++)
f[i][j][k][l]=std::max(f[i][j][k][l],f[i+dx[p]][j+dy[p]][k+dxf[p]][l+dyf[p]]);
f[i][j][k][l]+=(map[i][j]+map[k][l]);
if(i==k&&j==l)
f[i][j][k][l]-=map[i][j];
}
printf("%d",f[n][n][n][n]);
return 0;
}
空间控制在n^3就已经够了
#include
#include
#include
#include
typedef unsigned long long ull;
int const maxn=111,maxm=111,inf=0x1f1f1f1f;
int const dx[4]={0,0,-1,-1};
int const dy[4]={-1,-1,0,0};
int const dxf[4]={-1,0,-1,0};
int const dyf[4]={0,-1,0,-1};
int map[maxn][maxn],f[321][maxn][maxn];
int abs(int x)
{
if(x<0)
return -x;
return x;
}
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
scanf("%d",&map[i][j]);
for(int k=1;k<(n<<1);k++)
for(int i=1;i<=std::min(n,k);i++)
for(int j=1;j<=std::min(n,k);j++)
{
for(int p=0;p<4;p++)
{
if(i+dx[p]<1 || !(k-i+dy[p]) || j+dxf[p]<1 || !(k-i+dyf[p]))
continue;
f[k][i][j]=std::max(f[k][i][j],f[k-1][i+dx[p]][j+dxf[p]]);
}
f[k][i][j]+=abs(map[i][k-i+1]-map[j][k-j+1]);
}
printf("%d\n",f[2*n-1][n][n]);
return 0;
}
其实这题根本就是那道时代的眼泪,那道在ioi上横空出世的一道神题
但还是写出锅来了…
边界可以直接memset
必须从第一行开始搜,因为需要给他们赋上点权
从上往下搜比较暴力,从下往上搜写崩了,等等再调吧…
#include
#include
#include
#include
int const maxn=501,maxm=501,inf=0x1f1f1f1f;
int n,m,f[maxn][maxn],ans,w[maxn][maxn];
int max(int x,int y,int z)
{
return std::max(std::max(x,y),z);
}
int main()
{
memset(w,-0x1f,sizeof(w));
scanf("%d%d",&n,&m);
int x=(m/2+1);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
scanf("%d",&w[i][j]);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
f[i][j]+=max(f[i-1][j-1],f[i-1][j],f[i-1][j+1])+w[i][j];
printf("%d",max(f[n][x],f[n][x+1],f[n][x-1]));
return 0;
}
这道题一看似乎是搜索,然而2^n肯定是过不了的
观察数据,k<=100,应该想到是关于剩余容量可能性的dp
f[i][j][k]表示i,j处是否能得到k这个数
最朴素的转移是枚举所有状态,枚举上一次的所有可能性看看能不能得到当前状态,复杂度O(nmk^2)
不过我们发现,枚举当前状态效率很低,因为有很多废状态根本不可能由上一个转移而来吗
所以我们直接枚举上一次的所有状态即可,复杂度O(nmk)
稳如老狗
#include
#include
#include
#include
int const maxn=111,maxm=111,inf=0x1f1f1f1f;
int n,m,p,w[maxn][maxn],f[maxn][maxn][maxn],ans,anss[maxn];
int main()
{
scanf("%d%d%d",&n,&m,&p);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
scanf("%d",&w[i][j]);
f[1][1][w[1][1]%p]=1;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
for(int k=0;k
感觉这题递推很神,于是就写了比较友善的记搜
顺便剪了个并无卵用的枝
在条件允许的时候可以两次两次跳,特殊地,有两种可能跳回原地,应注意
#include
#include
#include
#include
int const maxn=501,maxm=501,inf=0x1f1f1f1f;
int n,m,f[maxn][maxn];
int dfs(int u,int step)
{
u+=u<=0?n:0;u+=u>n?-n:0;
if(f[u][step])
return f[u][step];
if(step==m)
return f[u][step]=u==1;
return f[u][step]=step==m-1?dfs(u+1,step+1)+dfs(u-1,step+1):dfs(u+2,step+2)+dfs(u-2,step+2)+dfs(u,step+2)*2;
}
int main()
{
scanf("%d%d",&n,&m);
printf("%d",dfs(1,0));
return 0;
}
递推写法
一直以为两层循环1-n在第一层无法转移
结果发现如果1-m在第一层就没问题了
第二次移动一定是由第一次移动更新而来
记搜是枚举点判断次数
递推应该反过来
#include
#include
#include
#include
int const maxn=501,maxm=501,inf=0x1f1f1f1f;
int n,m,f[maxn][maxn];
int dfs(int u,int step)
{
u+=u<=0?n:0;u+=u>n?-n:0;
if(f[u][step])
return f[u][step];
if(step==m)
return f[u][step]=u==1;
return f[u][step]=step==m-1?dfs(u+1,step+1)+dfs(u-1,step+1):dfs(u+2,step+2)+dfs(u-2,step+2)+dfs(u,step+2)*2;
}
int main()
{
scanf("%d%d",&n,&m);
f[1][0]=1;
for(int i=1;i<=m;i++)
{
f[1][i]=f[n][i-1]+f[2][i-1];
for(int j=2;j
很明显,50一定在100前
问题转化为括号匹配,然后直接套卡特兰数
f[i][j]表示已经拿了i张50,j张100
转移考虑对于每一种(i,j)都能从上一张的两种选择更新
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++)
f[i][j]=f[i-1][j]+f[i][j-1];
只有50的状态为初始状态
#include
#include
#include
#include
typedef long long ll;
int const maxn=501,maxm=501,inf=0x1f1f1f1f;
int n;
ll f[maxn][maxn];
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
f[i][0]=1;
for(int i=1;i<=n;i++)
for(int j=1;j<=i;j++)
f[i][j]=f[i-1][j]+f[i][j-1];
printf("%lld",f[n][n]);
return 0;
}
time是c的关键字!!!
事实证明能用贪心做的dp尽量分着顺序写,每一部分单独考虑
#include
#include
int const maxn=300110,maxm=111,inf=0x1f1f1f1f;
int m,s,tim,f[maxn];
int main()
{
scanf("%d%d%d",&m,&s,&tim);
for(int i=1;i<=tim;i++)
{
if(m>=10)
f[i]=f[i-1]+60,m-=10;
else
f[i]=f[i-1],m+=4;
}
//优先处理闪烁
for(int i=1;i<=tim;i++)
{
f[i]=std::max(f[i-1]+17,f[i]);
if(f[i]>=s)
{
puts("Yes");
printf("%d",i);
return 0;
}
}
puts("No");
printf("%d",f[tim]);
return 0;
}
事实证明,状态对于动态规划重要性远大于其他!
才不会说我搞了个走到第i个城市休息了j天的状态然后不会初始化就一直锅着
f[i][j]表示第i天在第j个城市处
#include
#include
#include
int const maxn=1011,maxm=111,inf=0x1f1f1f1f;
int n,m,np[maxn],w[maxn],f[maxn][maxn];
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&np[i]);
for(int i=1;i<=m;i++)
scanf("%d",&w[i]);
memset(f,0x1f,sizeof(f));
for(int i=0;i<=m;i++)
f[i][0]=0;
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++)
f[i][j]=std::min(f[i-1][j-1]+w[i]*np[j],f[i-1][j]);
printf("%d",f[m][n]);
return 0;
}
水题,不过考虑到用二维前缀和直接水岂不是很low做题的初心,还是写了些递推,然而根本不会
因为最大的正方形一定不是以0结尾
所以我们可以用f[i][j]表示右下角为(i,j)的最大正方形
右下角如果为零,显然就不符合状态 if(!a[i][j]) f[i][j]=0;
对于正方形,我们肯定是想考虑他的内部填充,但对于每一次扩大出来的点,还是要处理一下,作为左右边界
因此转移就可以表示为
f[i][j]=min(f[i-1][j-1], 从当前位置向上延伸连续的1的个数, 当前位置左侧延伸连续1的个数)
简化一下会发现后两条就是对正方形边缘的处理
等价于f[i][j]=min(f[i-1][j],f[i][j-1],f[i-1][j-1])+1;
#include
#include
int const maxn=111;
int f[maxn][maxn],n,m,ans;
int min(int x,int y,int z)
{
return std::min(std::min(x,y),z);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int a,j=1;j<=m;j++)
scanf("%d",&a),f[i][j]=a?min(f[i-1][j],f[i][j-1],f[i-1][j-1])+a:0,ans=std::max(ans,f[i][j]);
printf("%d",ans);
return 0;
}
这题感觉跟最大正方形1一毛一样啊,就是转移条件变了变
f[i][j]表示右下角在(i,j)长度最大正方形
对于每一个f[i][j],我们都希望去考虑他的子结构
包括
然后对(i,j)这个点特殊处理一下即可(显然只有(a[i][j] a[i-1][j-1]相等 a[i-1][j] a[i][j-1]相等,两对不等才合法)
转移方程
if(!(a[i][j]^a[i-1][j-1]^a[i-1][j]^a[i][j-1]))
f[i][j]=min(f[i-1][j-1],f[i-1][j],f[i][j-1])+1;
最后答案记得+1,原因很显然:没有初始化
#include
#include
#include
int const maxn=1611,inf=0x1f1f1f1f;
int f[maxn][maxn],a[maxn][maxn],n,m,ans;
int min(int x,int y,int z)
{
return std::min(std::min(x,y),z);
}
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++)
{
if(!(a[i][j]^a[i-1][j-1]^a[i-1][j]^a[i][j-1]))
f[i][j]=min(f[i-1][j-1],f[i-1][j],f[i][j-1])+1;
ans=std::max(f[i][j],ans);
}
if(!ans)
ans--;
//特判,原因显然是边长为1无法构成01相间的正方形
printf("%d",ans+1);
return 0;
}
二维前缀和的应用
先求出以(1,1)为左上角每个点为右下角的矩形的和
即求二维前缀和
b[i][j]=b[i-1][j]+b[i][j-1]-b[i-1][j-1]+a[i][j];
由于正方形的性质
我们只需要枚举右下角的坐标(x,y)
f[i][j]表示以(i,j)为右下角(i-c,j-c)为左上角的正方形的和
转移方程:f[i][j]=b[i][j]-b[i-c][j]-b[i][j-c]+b[i-c][j-c]
#include
#include
#include
int const maxn=1611,inf=0x1f1f1f1f;
int f[maxn][maxn],a[maxn][maxn],b[maxn][maxn];
int lastx,lasty,n,m,c,ans=-inf;
int main()
{
scanf("%d%d%d",&n,&m,&c);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
scanf("%d",&a[i][j]),b[i][j]=a[i][j]+b[i-1][j]+b[i][j-1]-b[i-1][j-1];
for(int i=c;i<=n;i++)
for(int j=c;j<=m;j++)
{
f[i][j]=b[i][j]-b[i-c][j]-b[i][j-c]+b[i-c][j-c];
if(ans
这题线性结构十分明显,每次转移都要遍历寻找上一次的状态
f[i]表示前i个人至少要分成多少个集合
ff[i]表示前i个人的至少有多少点危险度
易知,每一次的转移都要寻找到上一个集合的状态
因此就有两重循环
i : 1~n 遍历所有点
j : i~1 寻找最后一个集合(也就是当前点所在的集合)的开头元素,注意当最后一个集合元素种数超过k就break
转移很简单
因为j是最后一个集合的开头,j-1就是上一个集合的结尾
f[i]=std::min(f[i],f[j-1]+1);
ff[i]=std::min(ff[i],ff[j-1]+cnt);
//其中cnt为当前集合的元素种数
边界问题
而且由于j-1会访问到0,所以0要特殊处理
#include
#include
#include
int const maxn=1101,maxm=1101,inf=0x1f1f1f1f;
int n,V,f[maxn],ff[maxn],m,k,bel[maxm],cnt,ton[maxn];
int min(int x,int y,int z)
{
return std::min(std::min(x,y),z);
}
int main()
{
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=n;i++)
scanf("%d",&bel[i]);
memset(f,0x1f,sizeof(f));
memset(ff,0x1f,sizeof(ff));
f[0]=0,ff[0]=0;
for(int i=1;i<=n;i++)
{
cnt=0;
memset(ton,false,sizeof(ton));
for(int j=i;j>=1;j--)
{
if(!ton[bel[j]])
cnt++,ton[bel[j]]=true;
if(cnt>k)
break;
f[i]=std::min(f[i],f[j-1]+1);
ff[i]=std::min(ff[i],ff[j-1]+cnt);
}
}
printf("%d\n%d",f[n],ff[n]);
return 0;
}
宗教那道题的弱化版
开始直接把板子改了改码了上去,然后WA了0.5h…
后来当发现不满足条件时,不能直接break掉,因为有可能会反向来人导致条件又满足了,比如m=1时,当前有221,下一个是2,然而下4个是2111,这样条件又满足了
#include
#include
#include
int const maxn=2511,inf=0x1f1f1f1f;
int f[maxn],ff[maxn],ton[maxn],bel[maxn];
int n,m;
int min(int x,int y,int z)
{
return std::min(std::min(x,y),z);
}
int abs(int x)
{
return x>0?x:-x;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&bel[i]);
memset(f,0x1f,sizeof(f));
f[0]=0;
for(int i=1;i<=n;i++)
{
memset(ton,0,sizeof(ton));
for(int j=i;j>=1;j--)
{
ton[bel[j]]++;
if(abs(ton[1]-ton[2])<=m||!ton[1]||!ton[2])
f[i]=std::min(f[i],f[j-1]+1);
}
}
printf("%d",f[n]);
return 0;
}
当一个人比你小又比你强,那你就打不过他了
当一个人跟你一样强但比你小,那你就打不过他了*2
#include
#include
#include
#include
int const maxn=1001100,maxm=210,inf=0x1f1f1f1f;
int n,k,ans[maxn][2],a[maxn];
struct node
{
int tim,val;
node(int tim=0,int val=0):
tim(tim),val(val){}
};
std::dequeq;
std::dequep;
int main()
{
scanf("%d%d",&n,&k);
for(int x,i=1;i<=n;i++)
{
scanf("%d",&x);
while(!q.empty()&&q.back().val>=x)
q.pop_back();
while(!p.empty()&&p.back().val<=x)
p.pop_back();
q.push_back(node(i,x));
p.push_back(node(i,x));
while(i-k>=q.front().tim)
q.pop_front();
while(i-k>=p.front().tim)
p.pop_front();
if(i>=k)
ans[i][0]=q.front().val,
ans[i][1]=p.front().val;
}
for(int j=0;j<=1;j++)
{
for(int i=k;i<=n;i++)
printf("%d ",ans[i][j]);
puts("");
}
return 0;
}
强化版的单词背诵
// luogu-judger-enable-o2
#include
#include
#include
#include
int const maxn=2001000,maxm=210,inf=0x1f1f1f1f;
int n,k,a[2];
struct node
{
int tim,val;
node(int tim=0,int val=0):
tim(tim),val(val){}
};
std::dequeq;
int main()
{
scanf("%d%d%d",&n,&k,&a[1]);
puts("0");
for(int i=2;i<=n;i++)
{
scanf("%d",&a[i%2]);
while(!q.empty()&&a[(i-1)%2]<=q.back().val)
q.pop_back();
q.push_back(node(i-1,a[(i-1)%2]));
while(i-k>q.front().tim)
q.pop_front();
printf("%d\n",q.front().val);
}
return 0;
}
看似水题,实则巨坑 起码坑了我2h,且听我慢慢道来
选连续不超过k个的最大值可转化为踢掉最小值,被踢的牛满足两两距离小于k
转移方程f[i]=std::max(f[i],f[j]+w[i]) i-k-1<=j<=i-1
注意边界!!!
对于i,如果选了,则前边k个可以不选,所以可以从[i-k-1,i-1]中转移
另外,本题十分毒瘤,第9个点会莫名其妙的溢出,因此直接define int long long
莽上去
#include
#include
#include
#include
#define int long long
int const maxn=101100,maxm=210,inf=0x1f1f1f1f;
int n,k;
int sum,f[maxn],mn=inf*1000;
struct node
{
int tim,val;
node(int tim=0,int val=0):
tim(tim),val(val){}
};
std::dequeq;
signed main()
{
scanf("%lld%lld",&n,&k);
for(int i=1;i<=k+1;i++)
{
scanf("%lld",&f[i]),sum+=f[i];
while(!q.empty()&&q.back().val>=f[i]) q.pop_back();
q.push_back(node(i,f[i]));
}
for(int i=k+2;i<=n;i++)
{
scanf("%lld",&f[i]),sum+=f[i];
while(!q.empty()&&i-k-1>q.front().tim) q.pop_front();
f[i]+=q.front().val;
while(!q.empty()&&q.back().val>=f[i]) q.pop_back();
q.push_back(node(i,f[i]));
}
for(int i=n-k;i<=n;i++)
mn=std::min(mn,f[i]);
printf("%lld",sum-mn);
return 0;
}
事实证明,单调栈就是弱化版的单调队列,它无法控制区间长度
好吧我错了,单调栈确实可以用队列来实现,但这种结构仍然称为单调栈
按某一岸排序,在另一岸跑单调栈
#include
#include
#include
#include
#include
int const maxn=201000,maxm=210,inf=0x1f1f1f1f;
int n,ans,nor[maxn],sou[maxn],a[maxn],cnt;
int cmp(int x,int y)
{
return nor[x]q;
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d%d",&nor[i],&sou[i]),a[i]=i;
std::sort(a+1,a+1+n,cmp);
for(int i=1;i<=n;i++)
{
while(!q.empty()&&sou[a[i]]<=q.back())
q.pop_back(),cnt--;
q.push_back(sou[a[i]]),cnt++;
ans=std::max(ans,cnt);
}
printf("%d",ans);
return 0;
}
每次多加入一个元素就会对答案产生栈大小的贡献
#include
#include
#include
#include
#include
typedef long long ll;
int const maxn=111,maxm=210,inf=0x1f1f1f1f;
int n;
ll ans;
std::stacks;
int main()
{
scanf("%d",&n);
for(int a,i=1;i<=n;i++)
{
scanf("%d",&a);
while(!s.empty()&&a>=s.top())
s.pop();
ans+=s.size();
s.push(a);
}
printf("%lld",ans);
return 0;
}
单调队列完爆单调栈的惨剧
#include
#include
#include
#include
#include
int const maxn=100110,maxm=210,inf=0x1f1f1f1f;
int n,ans[maxn];
struct node
{
int t,v;
node(int t=0,int v=0):
t(t),v(v){}
};
std::dequeq;
int main()
{
scanf("%d",&n);
for(int a,i=1;i<=n;i++)
{
scanf("%d",&a);
while(!q.empty()&&a>q.back().v)
{
ans[q.back().t]=i;
q.pop_back();
}
q.push_back(node(i,a));
}
for(int i=1;i<=n;i++)
printf("%d\n",ans[i]);
return 0;
}
一眼望去,woc这不是悬线法嘛,定睛一看,似乎还真是单调栈的裸题…当然也是悬线法的裸题
维护一个高度单调递增的栈,每次弹栈时候更新,因为满足单增的性质,因此每个后面的元素都可以“使用”前面元素的高度,每次弹栈肯定是由于单增不满足,因此每一个被弹出栈的元素都可以“使用”当前引起弹栈的元素的高度,因此这个元素入栈时宽度为所有被弹元素之和+1
注意:每一行都是独立的,记得及时清栈
#include
#include
#include
#include
#include
int const maxn=1110,maxm=1110,inf=0x1f1f1f1f;
int n,m,ans=-1,bns=-1,lft[maxn][maxm],rit[maxn][maxm],up[maxn][maxm],f[maxn][maxm];
struct node
{
int len,ht;
node(int len=0,int ht=0):
len(len),ht(ht){}
};
std::stacks;
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
char a;
std::cin>>a;
if(a=='F')
up[i][j]=up[i-1][j]+1;
int l=0;
while(!s.empty()&&up[i][j]<=s.top().ht)
{
l+=s.top().len;
ans=std::max(ans,l*s.top().ht);
s.pop();
}
s.push(node(l+1,up[i][j]));
}
int l=0;
while(!s.empty())
{
l+=s.top().len;
ans=std::max(ans,l*s.top().ht);
s.pop();
}
}
printf("%d",ans*3);
return 0;
}
容易发现宽度不会影响答案
简单的性质:当两栋楼不一样高,一定需要2张海报
维护一个单调递增的队列,只需要搞一下相等的情况就好
#include
#include
#include
#include
int const maxn=111,maxm=210,inf=0x1f1f1f1f;
int n,ans;
std::stacks;
int main()
{
scanf("%d",&n);
for(int qaq,a,i=1;i<=n;i++)
{
scanf("%d%d",&qaq,&a);
while(!s.empty()&&s.top()>a)
{
ans++;s.pop();
}
while(!s.empty()&&s.top()==a)
s.pop();
s.push(a);
}
while(!s.empty()&&s.top()>-1)
{
ans++;
s.pop();
}
printf("%d",ans);
return 0;
}
不知道为什么枚举右端点然后把左端点进栈的做法不对…
0分代码,改天再调吧…
#include
#include
#include
#include
#include
int const maxn=1110,maxm=1110,inf=0x1f1f1f1f;
int n,m,ans=-1,bns=-1,lft[maxn][maxm],rit[maxn][maxm],up[maxn][maxm],f[maxn][maxm],pre[maxn][maxm];
inline int min(int x,int y,int z)
{
return std::min(std::min(x,y),z);
}
inline int pow(int x)
{
return x*x;
}
struct node
{
int pos,sum;
node(int pos=0,int sum=0):
pos(pos),sum(sum){}
};
std::dequeq;
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int a,j=1;j<=m;j++)
scanf("%d",&a),pre[i][j]=pre[i][j-1]+pre[i-1][j]-pre[i-1][j-1]+a;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
int hight=j-i+1;
for(int k=1;k<=m;k++)
{
int sum=pre[j][k]-pre[i][k];
int last=-1,now=0;
if(!q.empty())
last=q.front().pos;
while(!q.empty()&&q.back().sum>sum)
{
now=q.back().pos;
q.pop_back();
}
ans=std::max(ans,hight*(last-now+1));
q.push_back(node(k,sum));
}
if(!q.empty())
{
int last=q.front().pos,now=0;
while(!q.empty())
{
now=q.back().pos;
q.pop_back();
}
ans=std::max(ans,hight*(last-now+1));
}
}
printf("%d",ans);
return 0;
}
样例中这一段
“1,2,2”
第二个2与1之间并没有大于2的数,然而答案并没有统计上
实质上是二分答案,然后用dp检验可行性
对于大的背包来说,接口越大价值越大,满足单调性
直接二分答案+可行性01背包check是否可行
#include
#include
#include
int const maxn=1101,inf=0x1f1f1f1f;
int f[maxn],cv[maxn],w[maxn];
int n,p,V,mx=-1,ans;
int check(int mid)
{
memset(f,0,sizeof(f));
for(int i=1;i<=n;i++)
{
if(cv[i]>mid)
continue;
for(int j=V;j>=cv[i];j--)
f[j]=std::max(f[j],f[j-cv[i]]+w[i]);
}
for(int j=V;j>=1;j--)
if(f[j]>=p)
return true;
return false;
}
int main()
{
scanf("%d%d%d",&n,&p,&V);
for(int i=1;i<=n;i++)
scanf("%d%d",&cv[i],&w[i]),mx=std::max(mx,cv[i]);
int l=1,r=mx;
while(l<=r)
{
int mid=(l+r)/2;
if(check(mid))
ans=mid,
r=mid-1;
else
l=mid+1;
}
if(ans)
{
printf("%d",ans);
return 0;
}
printf("No Solution!");
return 0;
}
两种写法
一种是存长度,一种是存位置
其实一点差别都没有
以每个(i,j)为悬线的底,让他向左上右延伸
然而,这还不够
因为,l[i][j] 和 r[i][j] 的值都各自取决于 l[i-1][j] 和 r[i-1][j]。(因为为保证成为一个矩形,l[i][j] 不能超过 l[i-1][j],r 同理)
更新的时候顺便搞搞就好了
无法再裸
#include
#include
#include
int const maxn=1110,maxm=1110,inf=0x1f1f1f1f;
int n,m,ans,lft[maxn][maxm],rit[maxn][maxm],up[maxn][maxm];
char a[maxn][maxm];
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
rit[0][i]=m+1;
for(int i=1;i<=n;i++)
{
int t=0;
for(int j=1;j<=m;j++)
{
std::cin>>a[i][j];
if(a[i][j]=='R')
{
up[i][j]=0,
lft[i][j]=0;
t=j;
continue;
}
up[i][j]=up[i-1][j]+1;
lft[i][j]=std::max(t,lft[i-1][j]);
}
t=m+1;
for(int j=m;j;j--)
{
if(a[i][j]=='R')
{
rit[i][j]=m+1;
t=j;
continue;
}
rit[i][j]=std::min(t,rit[i-1][j]);
}
for(int j=1;j<=m;j++)
ans=std::max(ans,(rit[i][j]-lft[i][j]-1)*up[i][j]);
}
printf("%d",ans*3);
return 0;
}
相较于玉蟾宫,多了一个限制条件
#include
#include
#include
#include
#include
int const maxn=1110,maxm=1110,inf=0x1f1f1f1f;
int n,m,ans,lft[maxn][maxm],rit[maxn][maxm],up[maxn][maxm],a[maxm];
std::stacks;
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
rit[0][i]=n+1;
for(int i=1;i<=n;i++)
{
int pos=0;
for(int j=1;j<=m;j++)
{
scanf("%d",&a[j]);
if(!a[j])
{
up[i][j]=lft[i][j]=0;
pos=j;
continue;
}
up[i][j]=up[i-1][j]+1;
lft[i][j]=std::max(lft[i-1][j],pos);
}
pos=m+1;
for(int j=m;j;j--)
{
if(!a[j])
{
rit[i][j]=m+1;
pos=j;
continue;
}
rit[i][j]=std::min(rit[i-1][j],pos);
}
for(int j=1;j<=m;j++)
ans=std::max(ans,std::min(rit[i][j]-lft[i][j]-1,up[i][j]));
}
printf("%d",ans);
return 0;
}
正方形的性质+悬线法+不怎么巧妙地转移
#include
#include
#include
int const maxn=2511,maxm=2511,inf=0x1f1f1f1f;
int n,m,ans,lft[maxn][maxm],rit[maxn][maxm],up[maxn][maxm],f[maxn][maxm],ff[maxn][maxm],a[maxm];
inline int min(int x,int y,int z)
{
return std::min(std::min(x,y),z);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
scanf("%d",&a[j]);
if(a[j])
{
up[i][j]=lft[i][j]=0;
f[i][j]=min(up[i-1][j],lft[i][j-1],f[i-1][j-1])+1;
ans=std::max(ans,f[i][j]);
continue;
}
up[i][j]=up[i-1][j]+1;
lft[i][j]=lft[i][j-1]+1;
}
for(int j=m;j;j--)
{
if(a[j])
{
rit[i][j]=0;
ff[i][j]=min(up[i-1][j],rit[i][j+1],ff[i-1][j+1])+1;
ans=std::max(ans,ff[i][j]);
continue;
}
rit[i][j]=rit[i][j+1]+1;
}
}
printf("%d",ans);
return 0;
}
棋盘的反向同奇偶染色。用于解决交错矩形问题
然后我存长度莫名爆炸
还是位置好用…
#include
#include
#include
#include
#include
int const maxn=2100,maxm=2100,inf=0x1f1f1f1f;
int n,m,ans=-1,bns=-1,lft[maxn][maxm],rit[maxn][maxm],up[maxn][maxm],f[maxn][maxm],a[maxn][maxm];
std::stacks;
inline int min(int x,int y,int z)
{
return std::min(std::min(x,y),z);
}
inline int pow(int x)
{
return x*x;
}
inline void color(int op)
{
memset(up,0,sizeof(up));
memset(lft,0,sizeof(lft));
memset(rit,0,sizeof(rit));
for(int i=1;i<=m;i++)
rit[0][i]=m+1;
for(int i=1;i<=n;i++)
{
int pos=0;
for(int j=1;j<=m;j++)
{
if(a[i][j]!=op)
{
up[i][j]=lft[i][j]=0;
pos=j;
continue;
}
up[i][j]=up[i-1][j]+1;
lft[i][j]=std::max(pos,lft[i-1][j]);
}
pos=m+1;
for(int j=m;j;j--)
{
if(a[i][j]!=op)
{
rit[i][j]=m+1;
pos=j;
continue;
}
rit[i][j]=std::min(pos,rit[i-1][j]);
}
for(int j=1;j<=m;j++)
{
int x=rit[i][j]-lft[i][j]-1,y=up[i][j];
ans=std::max(ans,x*y);
bns=std::max(bns,pow(std::min(x,y)));
}
}
}
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]);
if((i+j)&1)
a[i][j]=1-a[i][j];
}
color(1);
color(0);
printf("%d\n%d",bns,ans);
return 0;
}
简单来说状压dp就是把状态压成一个维度,直接进行状态转移
因为转移时要枚举所有状态,因此复杂度是O(2^n)
的
写状压一定要注意运算符的优先级问题!
+
-
*
/
%
<<
>>
>
<
<=
>=
==
!=
&
|
^
&&
||
?:
=
+=
… &=
然而这东西太长了没啥用…
总结点性质
状压dp的入门题,细节还是挺多的
1种0不种,将每一行作为一个状态,每行都从上一行的合法状态转移而来
合法状态的定义:
1.包含初始状态
2.满足题目的性质(对于本题来说就是1不相邻)
Map[i]表示每行的初始状态
scanf("%d",&a),Map[i]=(Map[i]<<1)+a;
pan[j]表示状态j 在本行是否合法
pan[i]=!(i&i<<1 || i&i>>1);
f[i][j]表示在第i行状态为j时的方案数
转移的时候枚举每行和当前行的状态,当状态包含初始状态且在本行合法时,枚举上一行的状态,看看是否合法
#include
#include
#include
int const maxn=15,maxm=5010,inf=0x1f1f1f1f,p=100000000;
int n,m,pan[maxm],Map[maxn],f[maxn][maxm],mx;
inline void change(int x,int pos)
{
for(int i=pos;i>=0;i--)
printf("%d",x>>i&1);
}
//将一个数转化为二进制数
//用于查错,看看状态是否枚举正确
inline void readin()
{
for(int a,i=1;i<=n;i++)
for(int j=1;j<=m;j++)
scanf("%d",&a),Map[i]=(Map[i]<<1)+a;
for(int i=0;i<=mx;i++)
pan[i]=!(i&i<<1 || i&i>>1);
}
inline void solve()
{
for(int i=1;i<=n;i++)
for(int j=0;j<=mx;j++)
if((Map[i]&j)==j && pan[j])
for(int k=0;k<=mx;k++)
if(!(k&j))
f[i][j]=(f[i][j]+f[i-1][k])%p;
}
int main()
{
scanf("%d%d",&n,&m);
mx=(1<
窝好菜啊…这么道水题写了好久…
这题跟玉米田几乎一毛一样
注意一点:列数少,所以应该把每一行给压起来,这样可以枚举所有列的状态!
#include
#include
int const maxn=111,maxm=1533;
int n,m,mx;
int Map[maxn],Sum[maxm],Pan[maxm],State[maxm],cnt;
int f[2][maxm][maxm];
inline void change(int x)
{
for(int i=3;i>=0;i--)
printf("%d",x>>i&1);
return;
}
inline int get(int x)
{
int tot=0;
while(x>0)
{
tot+=x&1;
x>>=1;
}
return tot;
}
inline void rin()
{
scanf("%d%d",&n,&m);
mx=(1<>1||i&i>>2||i&i<<1||i&i<<2);
if(Pan[i])
State[++cnt]=i;
Sum[i]=get(i);
}
for(int i=1;i<=cnt;i++)
if((State[i]&Map[1])==State[i])
f[1][0][State[i]]=Sum[State[i]];
for(int i=1;i<=cnt;i++)
if((State[i]&Map[2])==State[i])
for(int j=1;j<=cnt;j++)
if((State[j]&Map[1])==State[j]&&!(State[j]&State[i]))
f[0][State[j]][State[i]]=std::max(f[0][State[j]][State[i]],f[1][0][State[j]]+Sum[State[i]]);
return;
}
inline void solve()
{
for(int i=3;i<=n;i++)
for(int j=1;j<=cnt;j++)
if((State[j]&Map[i])==State[j])
for(int k=1;k<=cnt;k++)
if((State[k]&Map[i-1])==State[k]&&!(State[k]&State[j]))
for(int l=1;l<=cnt;l++)
if((State[l]&Map[i-2])==State[l]&&!(State[l]&State[j])&&!(State[l]&State[k]))
f[i%2][State[k]][State[j]]=std::max(f[i%2][State[k]][State[j]],f[(i-1)%2][State[l]][State[k]]+Sum[State[j]]);
return;
}
inline void write()
{
int ans=0;
for(int i=1;i<=cnt;i++)
for(int j=1;j<=cnt;j++)
ans=std::max(f[n%2][State[i]][State[j]],ans);
printf("%d",ans);
return;
}
int main()
{
rin();
pretreatment();
solve();
write();
return 0;
}
f[i][j][k]表示是第i行状态的编号为j时总共选了k个国王
本来还想搞一发可行性背包
突然想到都枚举所有可能状态了还背个毛线包啊
遂无问津者
另,由以上三题不难看出状压dp中有关个数的部分一般都是找1的个数
若是答案就作为状态的值,若非则作为状态
#include
#include
int const maxn=150,maxm=1533;
int n,limit,mx;
int Sta[maxn],cnt,Sum[maxn];
long long f[12][maxm][maxn];
inline void change(int x)
{
for(int i=2;i>=0;i--)
printf("%d",x>>i&1);
return;
}
inline int get(int x)
{
int tot=0;
while(x)
{
tot+=x&1;
x>>=1;
}
return tot;
}
inline void rin()
{
scanf("%d%d",&n,&limit);
return;
}
inline void pretreatment()
{
mx=(1<>1))
{
Sta[++cnt]=i;
Sum[cnt]=get(i);
f[1][cnt][Sum[cnt]]=1;
}
return;
}
inline void solve()
{
for(int i=2;i<=n;i++)
for(int j=1;j<=cnt;j++)
{
int now=Sta[j];
for(int k=1;k<=cnt;k++)
{
int last=Sta[k];
if(!(now&last||now&last<<1||now&last>>1))
{
for(int l=0;l<=limit;l++)
f[i][j][l+Sum[j]]+=f[i-1][k][l];
}
}
}
return;
}
inline void write()
{
long long ans=0;
for(int i=1;i<=cnt;i++)
ans+=f[n][i][limit];
printf("%lld",ans);
}
int main()
{
rin();
pretreatment();
solve();
write();
return 0;
}
这个题刷新了我对状压dp的认识…
详细讲讲
f[i]表示i这个状态时能买到的序号(注意从1开始按顺序买)
怎么找上一个状态呢
比如10110这个状态
这三个1都可能是这次支付的,因此上个状态就是00110 10010 10100 这三个
怎么实现呢
我们可以搞个b数组,存只支付某一张钞票的状态,如001 010 100
我们把他跟状态i与一下,就找到了某一个1
注意f[i]存的是从1可以买到的商品序号
我们发现我们可以存一个前缀和,把上一个状态的前缀和(因为浪费的钱会影响转移)加上这次支付的面额来跟各个前缀和比较得到这次的商品序号
这道题无负数,前缀和满足单调递增,可以直接二分查找
复杂度O(2^(n+1)*logm)
#include
#include
int const maxn=100110,maxm=66536;
int n,m,mx,sum;
int f[maxm],pre[maxn],b[maxn],v[15],cv[maxn];
inline void change(int x)
{
for(int i=2;i>=0;i--)
printf("%d",x>>i&1);
printf(" ");
return;
}
inline int find(int x)
{
int l=0,r=m,ans=0;
while(l<=r)
{
int mid=l+r>>1;
if(pre[mid]<=x)
{
ans=mid;
l=mid+1;
}
else
r=mid-1;
}
return ans;
}
inline void rin()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&v[i]),sum+=v[i];
for(int i=1;i<=m;i++)
scanf("%d",&cv[i]),pre[i]=pre[i-1]+cv[i];
return;
}
inline void pretreatment()
{
mx=(1<
跟没有找零的转移思路有点像
都是从枚举状态i中的1,然后删掉某个1从而找到上一状态
f[i][j]表示状态i时最后一个奶牛序号是j的方案数
#include
#include
int const maxn=21,maxm=66536,maxe=25100;
long long f[maxm][maxn];
int Pos[maxn],a[maxn];
int n,K,mx;
inline void change(int x)
{
for(int i=4;i>=0;i--)
printf("%d",x>>i&1);
puts("");
}
inline int abs(int x)
{
return x<0?-x:x;
}
inline void rin()
{
scanf("%d%d",&n,&K);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
return;
}
inline void pretreatment()
{
mx=(1<K)
f[i][j]+=f[y][k];
}
}
}
inline void write()
{
long long ans=0;
for(int i=1;i<=n;i++)
ans+=f[mx][i];
printf("%lld",ans);
}
int main()
{
rin();
pretreatment();
solve();
write();
return 0;
}
这道题跟上边几道基本一样
f[i]表示i状态需要的最少分组
lftV[i]表示i状态能剩下的最大空间
转移的时候注意一点
f[i]更新的时候lftV[i]一定更新
f[i]不更新的lftV[i]不一定不更新!
我难道会说我就是因为忘了判新开一个组时f[i]不更新的情况而查了1h
#include
#include
#include
int const maxn=21,maxm=272144,maxe=501000,inf=0x1f1f1f1f;
int n,V,mx,cv[maxn];
int Pos[maxm];
long long lftV[maxm];int f[maxm];
inline void change(int x)
{
for(int i=4;i>=0;i--)
printf("%d",x>>i&1);
}
inline void rin()
{
scanf("%d%d",&n,&V);
for(int i=1;i<=n;i++)
scanf("%d",&cv[i]);
return;
}
inline void pretreatment()
{
mx=(1<=0)
{
if(f[i]>f[y])
f[i]=f[y],lftV[i]=lftV[y]-cv[j];
else
if(f[i]==f[y])
lftV[i]=std::max(lftV[i],lftV[y]-cv[j]);
}
else
{
if(f[i]>f[y]+1)
f[i]=f[y]+1,lftV[i]=V-cv[j];
else
if(f[i]==f[y]+1)
lftV[i]=std::max(lftV[i],lftV[y]-cv[j]);
// lftV[i]=std::max(lftV[i],V-cv[j]);
}
// printf("!!!%d %d\n",f[i],lftV[i]);
// puts("");
}
}
return;
}
inline void write()
{
// change(mx);
// puts("");
// printf("%d %d",f[mx],lftV[mx]);
printf("%d",f[mx]);
return;
}
int main()
{
rin();
pretreatment();
solve();
write();
return 0;
}
f[i][j]表示i状态时j为结束点的最小值
这题极限卡常,难受
#include
#include
#include
int const maxn=23,maxm=1148576,maxe=501000,inf=0x1f1f1f1f;
int n,map[maxn][maxn],mx;
int Pos[maxn];
int f[maxm][maxn];
inline void rin()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
scanf("%d",&map[i][j]);
return;
}
inline void pretreatment()
{
memset(f,0x1f,sizeof(f));
mx=(1<
这题不能填表!!!
像这样
#include
#include
#include
int const maxn=23,maxm=1148576,maxe=501000,inf=0x1f1f1f1f;
int n,l,mx,start[maxn][1101],last[maxn],num[maxn];
int Pos[maxn];
int f[maxm];
inline void change(int x)
{
for(int i=4;i>=0;i--)
printf("%d",x>>i&1);
puts("");
}
inline int get(int x)
{
int tot=0;
while(x)
{
tot+=x&1;
x>>=1;
}
return tot;
}
inline int find(int id,int check)
{
int ans=0,l=1,r=num[id]+1;
while(l<=r)
{
int mid=l+r>>1;
// printf("%d %d %d\n",l,r,mid);
// printf("!!! %d %d\n",start[id][1],last[id]);
if(start[id][mid]<=check&&start[id][mid]+last[id]>check)
{
l=mid+1;
ans=mid;
}
else
r=mid-1;
}
return ans;
}
inline void rin()
{
scanf("%d%d",&n,&l);
for(int i=1;i<=n;i++)
{
scanf("%d%d",&last[i],&num[i]);
for(int j=1;j<=num[i];j++)
scanf("%d",&start[i][j]);
}
return;
}
inline void pretreatment()
{
mx=(1<=0;i--)
if(f[i]>=l)
ans=std::min(ans,get(i));
if(ans==inf)
printf("-1");
else
printf("%d",ans);
return;
}
int main()
{
rin();
pretreatment();
solve();
write();
return 0;
}
因为对于某个状态来说,其所有子状态的最优并不能保证其最优
而当前状态的合法的下一个状态的最优值一定能使答案最优
(想一想,下一场电影的合法的开始时间中最晚的值一定能使结果最优)
好!从想出填表法到疯狂对拍后发现正解是刷表法共耗时2.5h
然后写完刷表法还是只有29分…
以下是对拍数据,耗时2.5h
5 88
10 3 0 17 25
20 3 2 22 48
30 4 3 27 33 34
40 2 1 15
50 3 0 17 38
ans=3
----------------
2 25
10 3 2 10 20
20 1 0
ans=2
----------------
6 95
10 1 1
20 4 2 11 37 65
30 2 0 5
40 3 3 53 62
50 2 0 27
60 2 3 29
ans=3
----------------
6 99
10 2 2 12
20 2 1 29
30 4 0 17 44 46
40 2 3 10
50 4 0 55 114 126
60 3 1 2 26
ans=3
----------------
6 77
10 3 2 3 7
20 4 3 14 36 57
30 1 0
40 1 2
50 1 0
60 1 2
ans=3
最大的锅是二分写炸了
统计答案要同时满足多个条件
但事实上应该满足start[id][mid]<=check就算合法的二分
只不过不一定是合法的答案而已
写完这道题感觉收获满满
#include
#include
#include
int const maxn=23,maxm=1148576,maxe=501000,inf=0x1f1f1f1f;
int n,l,mx,start[maxn][1101],last[maxn],num[maxn];
int Pos[maxn];
int f[maxm];
inline void change(int x)
{
for(int i=5;i>=0;i--)
printf("%d",x>>i&1);
}
inline int get(int x)
{
int tot=0;
while(x)
{
tot+=x&1;
x>>=1;
}
return tot;
}
inline int find(int id,int check)
{
int ans=0,l=1,r=num[id];
while(l<=r)
{
int mid=l+r>>1;
// printf("~~~%d\n",mid);
// printf("!!!%d %d\n",start[id][mid]<=check,start[id][mid]+last[id]>check);
// printf("???%d %d\n",start[id][mid],last[id]);
if(start[id][mid]<=check)
{
if(start[id][mid]+last[id]>=check)
ans=mid;
l=mid+1;
}
else
r=mid-1;
}
// printf("%d\n",ans);
return ans;
}
inline void rin()
{
scanf("%d%d",&n,&l);
for(int i=1;i<=n;i++)
{
scanf("%d%d",&last[i],&num[i]);
for(int j=1;j<=num[i];j++)
scanf("%d",&start[i][j]);
}
return;
}
inline void pretreatment()
{
mx=(1<=0;i--)
if(f[i]>=l)
ans=std::min(get(i),ans);
if(ans==inf)
printf("-1");
else
printf("%d",ans);
return;
}
int main()
{
// freopen("qwq.in","r",stdin);
// freopen("oup.out","w",stdout);
rin();
pretreatment();
solve();
write();
return 0;
}
最后附一个数据生成器吧,毕竟也花了不少精力在上面
#include
#include
#include
#include
#define rii register int i
#define p 6
#define pp 100
#define ppp 4
using namespace std;
long long seed;
int n,m;
int main()
{
freopen("data.in","w",stdout);//文件操作,得到输入文件
seed=time(0);
srand(seed);
n=rand();//windows下rand()max为32768,为了有一定的强度,我们乘一下
n*=4321;//n,m这里你也可以手动取值
n%=p;
n++;
m=rand();
m*=4321;
m%=pp;
m++;
printf("%d %d\n",n,m);
int w=10;
for(int i=1;i<=n;i++)
{
printf("%d ",w);
w+=10;
int ww=(rand()*4321)%ppp+1;
printf("%d ",ww);
int start=ppp;
int qwq=(rand()*4321)%start;
for(int j=1;j<=ww;j++)
{
printf("%d ",qwq);
qwq+=(rand()*4321)%w+1;
}
puts("");
}
return 0;
}
树若以某点为根,使得该树最大子树的结点数最小,那么这个点则为该树的重心,一棵树可能有多个重心。
1、树上所有的点到树的重心的距离之和是最短的,如果有多个重心,那么总距离相等。
2、插入或删除一个点,树的重心的位置最多移动一个单位。
3、若添加一条边连接2棵树,那么新树的重心一定在原来两棵树的重心的路径上。
定义几个数组: f [ u ] f[u] f[u]表示以u为根的总距离(深度和), s i z e [ u ] size[u] size[u]表示以u为根的子树的大小
首先我们任意以一个点为根dfs一遍,求出以该点为根的总距离。
转移考虑换根对深度和的影响,对于每个u能达到的点v,有:
f [ v ] = f [ u ] + s i z e [ 1 ] − s i z e [ v ] − s i z e [ v ] f[v]=f[u]+size[1]-size[v]-size[v] f[v]=f[u]+size[1]−size[v]−size[v]
解释一下, − s i z e [ v ] -size[v] −size[v]是因为v的子树到根的距离减少了1, + s i z e [ 1 ] − s i z e [ v ] +size[1]-size[v] +size[1]−size[v]则是v的子树之外的点到根的距离增加了1
dfs写法
因为要先处理叶子结点
因此可以在回溯的时候处理当前节点
#include
#include
#include
int const maxn=111111,maxm=111,inf=0x1f1f1f1f;
struct E
{
int to,next;
E(int to=0,int next=0):
to(to),next(next){}
}e[maxn<<1];
int head[maxn],cnt;
inline void add(int u,int v)
{
e[++cnt]=(E){v,head[u]};
head[u]=cnt;
}
int n;
int f[maxn][3],ans;
void dfs(int u,int fa)
{
f[u][1]=1;
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
if(v==fa)
continue;
dfs(v,u);
f[u][0]+=std::max(f[v][0],f[v][1]);
f[u][1]+=f[v][0];
}
}
int main()
{
scanf("%d",&n);
for(int u,v,i=1;i
怕爆栈?
拓扑序+树形DP教做人
需要先处理子节点,不妨从子节点向父节点建边
#include
#include
#include
int const maxn=6111,maxm=111,inf=0x1f1f1f1f;
int n;
int w[maxn],ind[maxn],fa[maxn];
int f[maxn][3],ans=-inf;
inline int max(int x,int y,int z)
{
return std::max(std::max(x,y),z);
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",w+i);
for(int s,fff,i=1;iq;
for(int i=1;i<=n;i++)
if(!ind[i])
q.push(i);
while(!q.empty())
{
int u=q.front();
int v=fa[u];
q.pop();
f[u][1]+=w[u];
ans=max(ans,f[u][1],f[u][0]);
ind[v]--;
f[v][0]+=std::max(f[u][1],f[u][0]);
f[v][1]+=f[u][0]>0?f[u][0]:0;
if(!ind[v])
q.push(v);
}
printf("%d",ans);
return 0;
}
树的重心+换根法dp
关键在于每个点可能有很多人
size和f初始化dfs的时候要注意
#include
#include
#include
int const maxn=111,maxm=111,inf=0x1f1f1f1f;
struct E
{
int to,next;
E(int to=0,int next=0):
to(to),next(next){}
}e[maxn<<1];
int head[maxn],cnt;
inline void add(int u,int v)
{
e[++cnt]=(E){v,head[u]};
head[u]=cnt;
}
int n,w[maxn];
int f[maxn],size[maxn];
int ans=inf;
void pre_dfs(int u,int fa,int dep)
{
size[u]=w[u];
//乘上人数
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
if(v==fa)
continue;
pre_dfs(v,u,dep+1);
size[u]+=size[v];
}
f[1]+=dep*w[u];
//乘上人数
}
void c_r_dfs(int u,int fa)
{
ans=std::min(ans,f[u]);
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
if(v==fa)
continue;
f[v]=f[u]+-size[v]+size[1]-size[v];
c_r_dfs(v,u);
}
}
int main()
{
scanf("%d",&n);
for(int l,r,i=1;i<=n;i++)
{
scanf("%d%d%d",w+i,&l,&r);
if(l) add(i,l);
if(r) add(i,r);
}
pre_dfs(1,0,0);
c_r_dfs(1,0);
printf("%d",ans);
return 0;
}
嘿嘿嘿我偏不写dfs
Bfs+topsort
喵的真难写,细节贼多,还是dfs好写
另外这题卡精度卡scanf,sb一个
#include
#include
#include
typedef unsigned long long ll;
int const maxn=1001100,maxm=111,inf=0x1f1f1f1f;
struct E
{
int to,next;
E(int to=0,int next=0):
to(to),next(next){}
}e[maxn<<1];
int head[maxn],cnt;
inline void add(int u,int v)
{
e[++cnt]=(E){v,head[u]};
head[u]=cnt;
}
struct node
{
int id,fa;
node(int id=0,int fa=0):
id(id),fa(fa){}
};
int n;
ll ind[maxn];
ll f[maxn];int size[maxn],dep[maxn];
int ans;ll max;
inline int read()
{
int re=0;
char ch=getchar();
while (ch<'0' || ch>'9') ch=getchar();
while (ch>='0' && ch<='9'){
re=re*10+ch-'0';
ch=getchar();
}
return re;
}
int main()
{
n=read();
for(int u,v,i=1;ip;
dep[0]=-1,p.push(node(1,0));
while(!p.empty())
{
int u=p.front().id,fa=p.front().fa;
p.pop();
dep[u]=dep[fa]+1;
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
if(v==fa)
continue;
p.push(node(v,u));
}
}
std::queueq;
if(ind[1]==1)
ind[1]++;
for(int i=1;i<=n;i++)
if(ind[i]==1)
q.push(i);
while(!q.empty())
{
int u=q.front();
q.pop();
size[u]++;
f[1]+=dep[u];
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
ind[v]--;
if(ind[v]<=0)
continue;
size[v]+=size[u];
if(ind[v]==1)
q.push(v);
}
}
p.push(node(1,0));
while(!p.empty())
{
int u=p.front().id,fa=p.front().fa;
p.pop();
if(maxu)
ans=u;
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
if(v==fa)
continue;
f[v]=f[u]-size[v]+size[1]-size[v];
p.push(node(v,u));
}
}
printf("%d",ans);
return 0;
}
神题
让我刚了2h的题
也是让我搞清了大部分树形dp思路的题
第一遍dfs求出子树内的信息
第二遍搞出非子树信息(一般会对子树内的产生影响,需要容斥)
f [ i ] [ j ] f[i][j] f[i][j]表示经过最多j步到i点的奶牛数
详见代码
#include
#include
#include
int const maxn=100110,maxm=24,inf=0x1f1f1f1f;
struct E
{
int to,next;
E(int to=0,int next=0):
to(to),next(next){}
}e[maxn<<1];
int head[maxn],cnt;
inline void add(int u,int v)
{
e[++cnt]=(E){v,head[u]};
head[u]=cnt;
}
int n,k,w[maxn];
int f[maxn][maxm],ff[maxn][maxm],fff[maxn][maxm],size[maxn];
void pre_dfs(int u,int fa)
{
fff[u][0]=f[u][0]=w[u];
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
if(v==fa)
continue;
pre_dfs(v,u);
for(int j=1;j<=k;j++)
f[u][j]+=f[v][j-1];
}
for(int j=1;j<=k;j++)
fff[u][j]=f[u][j]+fff[u][j-1];
//新开一个数组,要不会对向上回溯产生影响
}
void r_c_dfs(int u,int fa)
{
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
if(v==fa)
continue;
ff[v][0]=w[v];
ff[v][1]=fff[v][1]+fff[u][0];
for(int j=2;j<=k;j++)
ff[v][j]=fff[v][j]+ff[u][j-1]-fff[v][j-2];
//细节,对于u要用更新后的值,也就是ff数组
r_c_dfs(v,u);
}
}
int main()
{
scanf("%d%d",&n,&k);
for(int u,v,i=1;i
从下往上考虑,如果子树中有不同的时间,那么必须搞成一样的
f [ i ] f[i] f[i]表示i的子树内的最大边权
第一遍dfs处理f[i]
第二遍统计答案
#include
#include
#define int long long
int const maxn=500130,maxm=111,inf=0x1f1f1f1f;
inline int abs(int x)
{
return x>0?x:-x;
}
struct E
{
int to,next,w;
E(int to=0,int next=0,int w=0):
to(to),next(next),w(w){}
}e[maxn<<1];
int head[maxn],cnt;
inline void add(int u,int v,int w)
{
e[++cnt]=(E){v,head[u],w};
head[u]=cnt;
}
int n,root;
int f[maxn];
int ans;
int pre_dfs(int u,int fa)
{
int max=0;
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to,w=e[i].w;
if(v==fa)
continue;
max=std::max(max,pre_dfs(v,u)+w);
}
return f[u]=max;
}
void sv_dfs(int u,int fa)
{
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to,w=e[i].w;
if(v==fa)
continue;
ans+=f[u]-f[v]-w;
sv_dfs(v,u);
}
}
signed main()
{
scanf("%lld%lld",&n,&root);
for(int u,v,w,i=1;i
这题是个裸的可行性01依赖背包
然而我翻了翻题解,发现都没说到点上,所以我决定考前发一波福利
这道题实际上就是有依赖性质的机器分配(P2066),因此,依赖背包实质上就是一个树形分组背包,他与分组背包唯一的差别在于他的转移是树上相邻点的转移
结合代码解释一下为什么叫他分组背包
void dfs(int u)
{
//每个点都是一个组,代表以u为根的子树的最优解
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to;
//每次向与他相连的v转移
dfs(v);
for(int j=V+1;j>=1;j--)
//第二重循环枚举体积(第一重循环实质上就是dfs搜索点u)
for(int k=0;k
再说几点细节
AC代码
#include
#include
#include
int const maxn=324,maxm=324;
struct E
{
int to,next;
E(int to=0,int next=0):
to(to),next(next){}
}e[maxn];
int head[maxn],cnt;
inline void add(int u,int v)
{
e[++cnt]=(E){v,head[u]};
head[u]=cnt;
}
int n,V;
int f[maxn][maxm];
void dfs(int u)
{
for(int i=head[u];i;i=e[i].next)
//每一组
{
int v=e[i].to;
dfs(v);
for(int j=V+1;j>=1;j--)
//当前总体积:V+1!!!
for(int k=0;k
树形最大独立集问题
这题要求所有边都被看到,我们按从父向子树的方向考虑,对于某个点u,它所连接的每条边不被自己看到,就被儿子看到
我们发现对于当前点u的取舍,其实就是一种决策
状态
转移
#include
#include
int const maxn=111111;
struct E
{
int to,next;
E(int to=0,int next=0):
to(to),next(next){}
}e[maxn<<1];
int head[maxn],cnt;
inline void add(int u,int v)
{
e[++cnt]=(E){v,head[u]};
head[u]=cnt;
}
int n,kid[maxn][111],ind[maxn];
int f[maxn][2];
void dfs(int u)
{
f[u][1]=1,f[u][0]=0;
for(int i=1;i<=kid[u][0];i++)
{
int v=kid[u][i];
dfs(v);
f[u][1]+=std::min(f[v][0],f[v][1]);
f[u][0]+=f[v][1];
}
}
int main()
{
scanf("%d",&n);
for(int u,i=1;i<=n;i++)
{
scanf("%d",&u);scanf("%d",kid[u]);
for(int j=1;j<=kid[u][0];j++)
scanf("%d",kid[u]+j),ind[kid[u][j]]=true;
}
int root=0;
for(int i=0;i
树形dp求最大独立集神题,贪心水题
初始状态
以上是点i一定被覆盖到的情况
以上是i点不一定被覆盖到的情况
转移
简化状态
我们惊奇的发现,转移可以合并
f [ i ] [ j ] f[i][j] f[i][j]表示 f [ i ] [ 0... j ] ( j ∈ [ 2 , 4 ] ) f[i][0...j] ( j\in[2,4]) f[i][0...j](j∈[2,4])
简化后的转移