DP选讲
$DP$选讲
直接上题吧
放个题单
[各省省选DP](https://www.luogu.com.cn/training/151079)
$P5322[BJOI2019]$排兵布阵
一眼题,考虑$dp[i][j]$表示已经确定前$i$个的选的数量$j$的最大收益,考虑怎么转移
直接转移这一维和上一维的数量,枚举复杂度$O(n\times m^2)$
那么显然的是直接枚举有很多状态无用,那么有用的决策点只有$k$个
那么直接枚举决策点,那么非决策点必定不优,显然的是就是你在两个决策点之间
花费是无用的,那么复杂度变为$O(n\times m\times k)$
#include<bits/stdc++.h> #define MAXM 20005 #define MAXN 150 using namespace std; inline int rd(){ int x=0,f=1;char ch=getchar(); while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();} while (isdigit(ch)){x=x*10+ch-48;ch=getchar();} return x*f; } int jc[MAXN][MAXN]; int a,s,n,m,val[MAXN][MAXN],dp[2][MAXM]; int main() { cin>>s>>n>>m; for(int i=1;i<=s;i++) { for(int j=1;j<=n;j++) { a=rd(); jc[j][i]=a; } } for(int i=1;i<=n;i++) { sort(jc[i]+1,jc[i]+1+s); for(int j=1;j<=s;j++) { val[i][j]=2*jc[i][j]+1; } } for(int i=1;i<=n;i++) { int now=(i&1),pre=(now^1); // memset(dp[now],0xcf,sizeof(dp[now])); for(int k=0;k<=s;k++) { for(int j=0;j<=m;j++) { if(j<val[i][k]) { dp[now][j]=max(dp[now][j],dp[pre][j]); } else { dp[now][j]=max(dp[now][j],max(dp[pre][j],dp[pre][j-val[i][k]]+i*k)); } } } // for(int j=0;j<=m;j++) // { // cout<<dp[now][j]<<" "; // } // cout<<endl; } cout<<dp[n&1][m]<<endl; } /* 1 3 10 2 2 6 ./a.exe<a.in>a.out 2 3 10 2 2 6 0 0 0 */
$BJOI[2019]$奥术神杖
上来第一眼没有思路,由于直接$DP$显然不好求解,而且这个记录贡献的方式很生草
先考虑如果确定了最后状态,那么答案怎么求$?$
$\sqrt{\Pi_{i=1}^{|s|}v[i]}^{|s|}$
就是把所有出现的咒语乘起来,对总次数开根号
套路,取$log$
那么式子变为
$Ans=log(\sqrt{\Pi_{i=1}^{|s|}v[i]}^{|s|})$
$Ans=log((\Pi_{i=1}^{|s|}v[i])^\frac{1}{|s|})$
$Ans=\frac{1}{|s|}log(\Pi_{i=1}^{|s|}v[i])$
$Ans=\frac{1}{|s|}\sum_{i=1}^{|s|}log(v[i])$
(不得不说$log$乘法转加法真的很好用)
再次发现,这个东西无法直接转移
以我的理解是这个不满足最优子问题结构,无法设计一个好的状态去转移,就是说你用这个状态无法能去转移
就是无法设计一个状态去转移,就是即使你设计$dp[i][j]$也无法完整考虑个数这个问题
而且你次数与次数之间没办法转移,即使你当前保证是当前乘积最大值,也不能开完之后还是最大值
或许你可以换个状态,$dp[i][j]$表示确定了$i$位,匹配$j$个的乘积最大值
发现没办法转移,你需要确定这一位,然后并不知道匹配了多少个
你又说,再多设一维,并且把$dp$设在$AC$自动机上
$dp[i][j][k]$表示目前确定了$i$位,已经匹配了$j$个,目前在$AC$自动机的第$k$个节点的最大乘积
大概的转移方式就是$dp[i+1][j+val[y]][y]=dp[i][j][k]$
结果发现这个可以转移。。。但是你看看这个复杂度就很不友好
在极限数据下的话$dp$状态数是$O(n\times n^2\times cnt)$
分别是$n$个位置,一共所有可能匹配数目$n^2$,和$AC$自动机的节点数
那么用一个很好的套路,对于这种样子带着小数的$dp$,显然可以想到分数规划
然后判断是否合法吧
那么就是这个样子$\frac{1}{|s|}\sum_{i=1}^{|s|}log(v[i])>=mid$
$\sum_{i=1}^{|s|}(log(v[i])-mid)>=0$
那么对于每个贡献$-=mid$,然后转移这个式子的最大值,判断是否大于$0$
发现一个很有意思的事情,这个式子不存在那个烦人的开根号,只需要累和就好了
然后这个也不必关心匹配了几个,只需要一直累加就好了,貌似就解决了
大概重新透彻了分数规划的意义,就是某些状态转移需要和数量或其他有关联
那么就分数规划化简式子,换一种统计方法得到最优
也就是这道题,直接转移不好写,化简完式子之后发现只需要发现有了一个新的匹配直接加上新的匹配的贡献就好了
这个也很好的解决了,式子本身含义也是所有出现的贡献累加起来
这个式子的最大值很好转移
设$dp[i][j]$表示目前已经确定了$i$位,转移到了第$j$个节点的这个式子的最大值
那么转移的话也很简单了
而且这样转移和$AC$自动机匹配性质一样,显然可以得到所有的状态,而且贡献正确
#include<bits/stdc++.h> #define INF 2147473647 #define MAXN 2005 using namespace std; const double eps=1e-5; int tr[MAXN][15],End[MAXN],fail[MAXN],cnt,n; int zy[MAXN][MAXN][2]; double F[MAXN][MAXN],val[MAXN]; char T[MAXN]; void Insert(char *s,double w) { int now=0; for(int i=1;s[i];i++) { if(!tr[now][s[i]-'0']) { tr[now][s[i]-'0']=++cnt; } now=tr[now][s[i]-'0']; } End[now]++; val[now]+=w; } void Get_fail() { queue<int>q; int now=0; for(int i=0;i<10;i++) { if(tr[now][i]) { q.push(tr[now][i]); } } while(!q.empty()) { int now=q.front(); End[now]+=End[fail[now]]; val[now]+=val[fail[now]]; q.pop(); for(int i=0;i<10;i++) { if(tr[now][i]) { fail[tr[now][i]]=tr[fail[now]][i]; q.push(tr[now][i]); } else { tr[now][i]=tr[fail[now]][i]; } } } } char Ans[MAXN]; double dp(double v) { for(int i=0;i<=cnt;i++) { val[i]-=End[i]*v; } for(int i=0;i<=n;i++) { for(int j=0;j<=cnt;j++) { F[i][j]=-1e100; } } F[0][0]=0; for(int i=0;i<n;i++) { for(int j=0;j<=cnt;j++) { if(F[i][j]>-1e99) { for(int k=0;k<10;k++) { if(T[i]!='.'&&T[i]!=k+'0') continue; int y=tr[j][k]; if(F[i+1][y]<F[i][j]+val[y]) { F[i+1][y]=F[i][j]+val[y]; zy[i+1][y][0]=j; zy[i+1][y][1]=k; //¼ÇÂ¼×ªÒÆµã } } } } } for(int i=0;i<=cnt;i++) { val[i]+=End[i]*v; } int pos=0; for(int i=1;i<=cnt;i++) { if(F[n][i]>F[n][pos]) pos=i; } for(int i=n,now=pos;i;i--) { Ans[i]=zy[i][now][1]+'0'; now=zy[i][now][0]; } return F[n][pos]; } int m,v; char s[MAXN]; int main() { cin>>n>>m; scanf("%s",T); for(int i=1;i<=m;i++) { scanf("%s",s+1); scanf("%d",&v); Insert(s,log(v)); } Get_fail(); double l=0,r=log(INF),res=0; while(r-l>eps) { double mid=(l+r)/2.0; if(dp(mid)>0) res=mid,l=mid; else r=mid; } dp(res); printf("%s",Ans+1); }
$ZJOI2019$线段树
也不知道$ZJ$出题人怎么想的,一天三道题都是$DP$
这个还是比较简单了
一个很显然的套路,就是可以考虑换一种枚举方式统计贡献
这个不是每个线段树有多少节点被打标记吗
转化思路为这个节点被多少个线段树打标记去累和
那么$dp[i][j]$表示第$i$个节点在前$j$次复制后有多少个被打标记
那么转移就很显然了
//真是不知道能在线段树上跑DP的出题人怎么想 //首先,这个东西肯定不能每次都重开线段树 //那么在一棵线段树上转移 //dp[now][i]当前节点在前i次操作覆盖了几次 //那么这么显然可以直接转移吧 //我们相当于知道了前面所有线段树的这个点状态 //那么由于转移是独立的,那么该咋搞咋搞 //我在想这个东西貌似不太对,就是说会不会出现 //貌似没有影响,因为不会出现标记为2 //我貌似又忘记一个东西叫,贡献独立 //既然整体不好搞,那我们很显然可以单独计算每一个贡献累加 #include<bits/stdc++.h> #define int long long #define mod 998244353 #define MAXN 200005 #define ls (now<<1) #define rs ((now<<1)|1) using namespace std; struct node { int l,r,sum; }tr[MAXN<<2]; int f[MAXN<<2],g[MAXN<<2],tf[MAXN<<2],tg[MAXN<<2],cnt; void build(int now,int l,int r) { tr[now].l=l,tr[now].r=r; f[now]=0; g[now]=1; tf[now]=1; tg[now]=1; if(l==r) return ; int mid=(l+r)>>1; build(ls,l,mid); build(rs,mid+1,r); } void pdTf(int now,int x) { (tr[now].sum*=x)%=mod; (tf[now]*=x)%=mod; (f[now]*=x)%=mod; } void pdTg(int now,int x) { (tg[now]*=x)%=mod; (g[now]*=x)%=mod; } void pd(int now) { if(tf[now]!=1) { pdTf(ls,tf[now]); pdTf(rs,tf[now]); tf[now]=1; } if(tg[now]!=1) { pdTg(ls,tg[now]); pdTg(rs,tg[now]); tg[now]=1; } } void push_up(int now) { tr[now].sum=(f[now]+tr[ls].sum+tr[rs].sum)%mod; } void change(int now,int l,int r) { pd(now); if(tr[now].l==l&&tr[now].r==r) { f[now]=(f[now]+cnt)%mod; g[now]=g[now]%mod; pdTf(ls,2); pdTf(rs,2); } else { int mid=(tr[now].l+tr[now].r)>>1; g[now]=(g[now]+cnt)%mod; if(r<=mid) { change(ls,l,r); pd(rs); f[rs]=(f[rs]+cnt+mod-g[rs])%mod; g[rs]=(g[rs]+g[rs])%mod; pdTf(rs<<1,2);pdTg(rs<<1,2); pdTf(rs<<1|1,2);pdTg(rs<<1|1,2); push_up(rs); } else if(l>mid) { change(rs,l,r); pd(ls); f[ls]=(f[ls]+cnt+mod-g[ls])%mod; g[ls]=(g[ls]+g[ls])%mod; pdTf(ls<<1,2);pdTg(ls<<1,2); pdTf(ls<<1|1,2);pdTg(ls<<1|1,2); push_up(ls); } else { change(ls,l,mid); change(rs,mid+1,r); } } push_up(now); } int n,m,opt,l,r; signed main() { cin>>n>>m; build(1,1,n); cnt=1; for(int i=1;i<=m;i++) { cin>>opt; if(opt==1) { cin>>l>>r; change(1,l,r); cnt=(cnt+cnt)%mod; } if(opt==2) { cout<<tr[1].sum<<endl; } } }
十二省联考2019皮配
简化题意就是每个人有两个性质,每个人有一个体积,还有一些人有性质限制,每种性质有体积限制,最后让求所有的方案数,同一个城市的第一个性质必须相同
那么就可以转化为,每个人是豆子,同一个城市里的是豆荚,同一豆荚里面的颜色相同,最后每种性状有重量限制,问最后的方案数
复杂度O(n\times M\times M)的暴力DP很好给出,设dp[i][j][k]表示已经放了i个物品,黄色的有j重量,圆粒的有k重量的方案数,每次枚举整个豆荚放入哪个颜色,然后乘上放入的圆皱方案数
首先不考虑有限制的豆子
考虑这个东西可以先把豆荚染色,然后由于第二性征和第一性征没有联系,那么就考虑分开转移,然后方案相乘
复杂度O(n\times m)
发现没办法处理有限制的豆子
那么对于无毒的豆荚,可以继续按上面的转移,豆子也是
对于有毒的豆荚,因为有限制,那么需要暴力转移
dp[i][j][k]表示已经在有毒的选了i个,绿色的有j的重量,圆粒的有k的重量的方案数
大概转移dp[i][j][k]=dp[i-1][j-valj][k-valk]+...
然后考虑把背包合并,大概就是枚举无毒的选绿色有多少重量,选圆粒的有多少重量
然后在重量允许的范围内枚举,有毒的选的重量,然后相乘得到总方案
大概就是几个背包的合并
一些细节问题,就是说这个转移颜色的时候,显然是一个豆荚一个豆荚转移,如果这个豆荚有一个有问题,就得全部摘出来
那么还有就是最后怎么合并,大概就是枚举那些有问题的豆荚是怎么选择的颜色,然后现在颜色已经确定了,然后再枚举有问题的豆子选哪个颜色,就好了
还有就是这个DP,第一维和第二维是毫无关系的,而且你这个第一维相对于豆荚转移,而第二维相对于豆子去转移
也就是最后合并的时候,我们两个维度表示的总数可能没关系,第一维表示总数是所有限制豆荚,第二维表示的总数是所有限制豆子
那么就显然了
被卡了好久,我一直没看明白G表示的是什么
这个东西不就是我写的超长的转移吗qaq,我自己没看出来
#include<bits/stdc++.h> #define int long long #define mod 998244353 #define MAXN 2505 using namespace std; int n,c,b[MAXN],s[MAXN],f[MAXN],g[MAXN],pre_f[MAXN],pre_g[MAXN],ht[MAXN]; int city_ht[MAXN],city_sum[MAXN],F[MAXN][MAXN],G[MAXN][MAXN]; void sol() { cin>>n>>c; int C0,C1,D0,D1,All=0; for(int i=1;i<=c;i++) { city_ht[i]=false; city_sum[i]=0; } cin>>C0>>C1>>D0>>D1; for(int i=1;i<=n;i++) { cin>>b[i]>>s[i]; ht[i]=-1; city_sum[b[i]]+=s[i]; All+=s[i]; } int K,x; cin>>K; for(int i=1;i<=K;i++) { cin>>x; cin>>ht[x]; city_ht[b[x]]=true; } memset(f,0,sizeof(f)); pre_f[0]=f[0]=1; for(int i=1;i<=c;i++) { if(city_ht[i]||(!city_sum[i])) continue; for(int j=C0;j>=city_sum[i];j--) { f[j]=(f[j]+f[j-city_sum[i]])%mod; } } for(int i=1;i<=C0;i++) pre_f[i]=(pre_f[i-1]+f[i])%mod; memset(g,0,sizeof(g)); pre_g[0]=g[0]=1; for(int i=1;i<=n;i++) { if(ht[i]==-1) { for(int j=D0;j>=s[i];j--) { g[j]=(g[j]+g[j-s[i]])%mod; } } } for(int i=1;i<=D0;i++) pre_g[i]=(pre_g[i-1]+g[i])%mod; int Cs=0,Ss=0; memset(F,0,sizeof(F));F[0][0]=1; memset(G,0,sizeof(G)); for(int ct=1;ct<=c;ct++) { if(city_ht[ct]) { Cs+=city_sum[ct]; Cs=min(Cs,C0); for(int i=0;i<=Cs;i++) { for(int j=0;j<=Ss;j++) { G[i][j]=F[i][j]; } } for(int a=1;a<=n;a++) { int t=s[a]; if(b[a]==ct&&ht[a]!=-1) { Ss+=s[a],Ss=min(Ss,D0); if(ht[a]==1) { for(int i=0;i<=Cs;i++) { for(int j=Ss;j>=t;j--) { F[i][j]=F[i][j-t]; } for(int j=t-1;j>=0;j--) F[i][j]=0; } } if(ht[a]>=2) { for(int i=0;i<=Cs;i++) { for(int j=Ss;j>=t;j--) { F[i][j]=(F[i][j]+mod+F[i][j-t])%mod; } } } if(ht[a]==3) { for(int i=0;i<=Cs;i++) { for(int j=Ss;j>=t;j--) { G[i][j]=G[i][j-t]; } for(int j=t-1;j>=0;j--) G[i][j]=0; } } if(ht[a]<=1) { for(int i=0;i<=Cs;i++) { for(int j=Ss;j>=t;j--) { G[i][j]=(G[i][j]+mod+G[i][j-t])%mod; } } } } } for(int j=0,t=city_sum[ct];j<=Ss;j++) { for(int i=Cs;i>=t;i--) F[i][j]=F[i-t][j]; for(int i=t-1;i>=0;i--) F[i][j]=0; } for(int i=0;i<=Cs;i++) { for(int j=0;j<=Ss;j++) { F[i][j]=(F[i][j]+mod+G[i][j])%mod; } } } } int res=0; for(int i=0;i<=Cs;i++) { for(int j=0;j<=Ss;j++) { int l1=max(0ll,All-C1-i),r1=C0-i;if(l1>r1) continue; int l2=max(0ll,All-D1-j),r2=D0-j;if(l2>r2) continue; int vf=pre_f[r1]; if(l1) vf=(vf+mod-pre_f[l1-1])%mod; int vg=pre_g[r2]; if(l2) vg=(vg+mod-pre_g[l2-1])%mod; res=(res+mod+vf*vg%mod*F[i][j]%mod)%mod; } } cout<<res<<endl; } int T; signed main() { cin>>T; while(T--) sol(); } /* 1 10 10 100 98 100 93 5 10 4 5 1 10 3 10 7 10 7 10 5 10 3 10 4 10 1 10 5 1 1 3 2 5 1 4 0 2 2 */
省选联考$2021$滚榜
会了费用提前就很简单了
//有个东西叫费用提前 //当你枚举这个选了多少个的时候,为了保证后面的也是 //那么后面的就统一加上,然后那么这个时候差不变 //该加多少还是加多少 #include<bits/stdc++.h> #define int long long using namespace std; int dp[1<<13][15][505],a[15],Ans,n,m; int lowbit(int x) { return x&(-x); } int Count(int num) { int res=0; while(num) { res++; num-=lowbit(num); } return res; } signed main() { cin>>n>>m; int Max=0; a[0]=-1; for(int i=1;i<=n;i++) { cin>>a[i]; if(a[i]>a[Max]) { Max=i; } } for(int i=1;i<=n;i++) { int tar=n*(a[Max]-a[i]+(Max<i)); if(tar<=m) { dp[1<<(i-1)][i][tar]=1; } } for(int i=1;i<=(1<<n)-1;i++) { int Num=Count(i); for(int j=1;j<=n;j++) { if((i>>(j-1))&1) { for(int k=0;k<=m;k++) { for(int z=1;z<=n;z++) { if(!((i>>(z-1))&1)) { int tar=k+(n-Num)*max(0ll,a[j]-a[z]+(j<z)); if(tar<=m) dp[i|(1<<(z-1))][z][tar]+=dp[i][j][k]; } } } } } } for(int i=1;i<=n;i++) { for(int j=0;j<=m;j++) { Ans+=dp[(1<<n)-1][i][j]; } } cout<<Ans<<endl; }

浙公网安备 33010602011771号