关于dp
发扬多头精神,质疑dp,理解dp,成为dp!
由浅入深
ATcoder Dp 普及~提高的版子记录
- A - Frog 1
和走楼梯很像的最优式dp
发现每一个石头只与前两个有关,那么选取前两个的dp值于高度差的和作动态调整即可
转移显然\(O(1)\)
int n=read(),h[M],f[M];
signed main(){
rep(i,1,n,1){
h[i]=read();
if(i>1) f[i]=f[i-1]+abs(h[i]-h[i-1]);
if(i>2) f[i]=min(f[i],f[i-2]+abs(h[i]-h[i-2]));
}wr(f[n]),pr(10);
return 0;
}
- B - Frog 2
这种题其实最优的是单调队列,也就是P1725 琪露诺
但是一般也会放数据结构过去,考虑区间查最大,可以做到\(\log n\)的转移
转移\(O(1)\to\)单调队列,\(O(\log n)\to\) ds
int n=read(),k=read(),h[M],f[M];
signed main(){
rep(i,1,n,1){
h[i]=read();
f[i]=((i==1)?0:INF);
rep(fr,1,k,1) if(i>fr)
f[i]=min(f[i],f[i-fr]+abs(h[i]-h[i-fr]));
}
wr(f[n]),pr(10);
return 0;
}
- C - Vacation
基础的动态规划概念
记录每天的三样事情最优解,转移从上一天不同的最优状态转移,显然\(O(1)\)
int n=read(),dp[M][3],a,b,c;
signed main(){
rep(i,1,n,1){
a=read(),b=read(),c=read();
dp[i][0]=max(dp[i-1][1],dp[i-1][2])+a;
dp[i][1]=max(dp[i-1][0],dp[i-1][2])+b;
dp[i][2]=max(dp[i-1][1],dp[i-1][0])+c;
}
wr(max(dp[n][0],max(dp[n][1],dp[n][2])));
return 0;
}
- D - Knapsack 1
背包的版子题,背包的动规用一句话概括就是\(\to\) 该重量的最大价值
对于所有物品去枚举可能重量,\(n\times v\) 的复杂度,\(v\) 是最大重量
int n=read(),W=read(),f[M],w,v,ans;
signed main(){
me(f,-1);f[0]=0;
rep(i,1,n,1){
w=read(),v=read();
dep(j,W,w,1) if(f[j-w]==-1) continue;
else f[j]=max(f[j],f[j-w]+v),ans=max(ans,f[j]);
}
wr(ans);
return 0;
}
- E - Knapsack 2
转换背包的动规思想,\(\to\) 得到该价值的最小重量
那么只要重量比我小,我就可以取这个价值,这样复杂度是 \(n\times w\)的,w是最大价值
int n=read(),W=read(),w,v,f[M],ans=0;
signed main(){
me(f,0x3f);f[0]=0;
rep(i,1,n,1){
w=read(),v=read();
dep(j,100000,v,1){
if(f[j-v]>INF) continue;
else f[j]=min(f[j-v]+w,f[j]);
if(f[j]<=W) ans=max(ans,j);
}
}
wr(ans),pr(10);
return 0;
}
- F - LCS
LIS问题,考虑对字符串dp,查了很多资料,在算法竞赛中,LIS问题最多只能做到\(n\times m\)的
其中 n,m是两个串的长度,这个dp方程很版,要注意转移顺序
其实也就是在i,j未完成匹配的最大匹配数
\[f_{i,j}=\begin{cases}f_{i-1,j-1}+1&s[i]=t[i]\\\max{f_{i,j-1},f_{i-1,j}}&s[i]\ne t[i]\end{cases}
\]
string s,t,qqt;
int dp[3005][3005],f[3005][3005];
stack<char>q;
inline void dfs(int i,int j){
if(i<1||j<1||dp[i][j]==0) return void();
if(s[i]==t[j]) q.push(s[i]);
if(f[i][j]==1) dfs(i-1,j);
if(f[i][j]==2) dfs(i,j-1);
if(f[i][j]==3) dfs(i-1,j-1);
}
signed main(){
s+='@';t+='@';
cin>>qqt;s+=qqt;
cin>>qqt;t+=qqt;
int n=(int)s.size()-1,m=(int)t.size()-1;
for(int i=1;i<=n;i++) for(int j=1;j<=m;j++)
if(s[i]==t[j]) dp[i][j]=dp[i-1][j-1]+1,f[i][j]=3;
else dp[i][j]=max(dp[i-1][j],dp[i][j-1]),
f[i][j]=(dp[i][j]==dp[i-1][j]?1:2);
dfs(n,m);
while(!q.empty()) cout<<q.top(),q.pop();
return 0;
}
- G - Longest Path
这个东西是有向图+没有环
那么拓扑序跑一遍,更新最长链即可
int n=read(),m=read(),f[M],in[M],ans;
vector<int>Ed[M];queue<int>q;
signed main(){
rep(i,1,m,1){
int u=read(),v=read();
Ed[u].pb(v);in[v]++;
}
rep(i,1,n,1)if(in[i]==0) q.push(i);
while(!q.empty()){
int u=q.front();q.pop();
for(auto v:Ed[u]){
f[v]=max(f[v],f[u]+1);in[v]--;
ans=max(ans,f[v]);
if(in[v]==0) q.push(v);
}
}
wr(ans),pr(10);
return 0;
}
- H - Grid 1
这个一样的,\(f_{i,j}=f_{i,j-1}+f_{i-1,j}\)
由于只能向下向右,所以到这个点的路线数量就是来的总路线数量
int H=read(),W=read(),dp[1005][1005],mp[1005][1005],vis[1005][1005];
queue<pii>q;inline void ad(int &x,int y,int z){
x+=y;x+=z;if(x>mod) x=x-mod;
}
signed main(){
rep(i,1,H,1) rep(j,1,W,1){
char ch;cin>>ch;mp[i][j]=(ch=='#'?0:1);
}
q.push(mk(1,1));dp[1][1]=1;
while(!q.empty()){
auto [i,j]=q.front();q.pop();
ad(dp[i][j],dp[i-1][j],dp[i][j-1]);
if(mp[i+1][j]==1&&!vis[i+1][j]) q.push(mk(i+1,j)),vis[i+1][j]=1;
if(mp[i][j+1]==1&&!vis[i][j+1]) q.push(mk(i,j+1)),vis[i][j+1]=1;
}wr(dp[H][W]),pr(10);
return 0;
}
- I - Coins
简单概率dp入门题,考虑每次掷硬币后正面朝上 \(k\) 个的概率
方程就非常简单\(f_{i}=f_{i-1}\times(p)+f{i}\times(1-p)\)
每次这样\(n^2\)遍历一遍即可
int n=read();lb dp[3005],x,ans;
signed main(){
dp[0]=1;
rep(i,1,n,1){
cin>>x;
dep(j,i,1,1) dp[j]=dp[j-1]*x+dp[j]*(1-x);
dp[0]=dp[0]*(1-x);
}
rep(i,1,n,1) if(i>n-i) ans+=dp[i];
printf("%.15Lf",ans);
return 0;
}
- J - Sushi
期望dp入门题
期望:概率乘上答案值的总和
解释一下吧
- 概率(Probability)
描述某个事件发生的可能性大小,取值在\([0,1]\)。
使用\(P(X)\)表示事件\(X\)发生的可能性
比如抛一枚质地均匀的硬币,正反两面出现的可能性都是 \(\frac{1}{2}=0.5\)
使用概率的说法就是 \(P(抛硬币抛出正面)=0.5\)
扔一枚六点骰子,那么就可以说 \(P(扔出1点)=\frac{1}{6}\) - 期望(Expectation)
期望是随机变量取值的加权平均,权重是取该值的概率。
对于离散随机变量 \(X\) ,它的期望公式为 $E[X]=\sum_{k}k\times P(X=k) $
什么叫离散随机变量?
就是这个事件可能出现的结果表达
比如我们用 \(X=1\) 表示抛硬币抛出了正面 ,\(X=0\) 表示抛出反面
\(E[抛硬币]=0\times P(抛出反面)+1 \times P(抛出正面)\)
由于正反面概率相同,那么可以得出\(E[抛硬币]=0\times 0.5+1\times 0.5=0.5\)
也就是说,一直抛硬币,正面为1,反面为0,最后加起来的平均数基本是0.5
同理,我们用1,2,3,4,5,6表示掷骰子掷出六个数值
\(E[掷骰子]=1\times \frac{1}{6}+2\times \frac{1}{6}+3\times \frac{1}{6}+4\times \frac{1}{6}+5\times \frac{1}{6}+6\times \frac{1}{6}=3.5\)
也就是说,一直掷骰子,最后骰子上的值加起来的平均数基本是3.5
期望反映的是长期重复试验中随机变量的平均结果。 - 区别
期望求取要用到概率,而求概率不需要期望
概率是具体结果的可能性,期望是长期平均值 - 结论
期望是加权平均数,把结果乘上概率加起来就是期望了 - 例子的具体深入
比如说这道题,也就是问你平均掷多少次骰子才可以把寿司吃完
那么考虑开一个三维的\(f_{i,j,k}\)数组,表示还有 \(i\) 个1分盘, \(j\) 个二分盘, \(k\)个 三分盘的期望步数
记住期望dp的重要建模套路,dp递推!
将当前状态到目标结束所需要的期望次数分解成 - 当前一步(消耗1次)
- 从新状态到结束状态的期望次数
\(E[当前状态]=1+\sum_{新状态}(P(转移到新状态)\times E[新状态])\)
本题以\(f_{i,j,k}\)作为状态,新状态有
\(f_{i-1,j,k}\)(吃了一个1分装)
\(f_{i+1,j-1,k}\)(吃了一个2分装)
\(f_{i,j+1,k-1}\)(吃了一个3分装)
就可以得到方程了:
\[E[i,j,k]=1+\frac{空盘数}{n}\times E[i,j,k]+\frac{i}{n}\times E[i-1,j-1,k]+\frac{j}{n}\times E[i+1,j-1,k-1]+\frac{k}{n}\times E[i,j+1,k-1]
\]
- 记住边界条件是\(f_{0,0,0}\)
递推或是记搜都可以
double f[305][305][305];
int a[5],n;
int main(int argc,char const *argv[]){
scanf("%d",&n);
for(int i=1,x;i<=n;++i){
scanf("%d",&x);
a[x]++;
}
for(int k=0;k<=n;++k){
for(int j=0;j<=n;++j){
for(int i=0;i<=n;++i){
if(i||j||k){
if(i)f[i][j][k]+=f[i-1][j][k]*i/(i+j+k);
if(j)f[i][j][k]+=f[i+1][j-1][k]*j/(i+j+k);
if(k)f[i][j][k]+=f[i][j+1][k-1]*k/(i+j+k);
f[i][j][k]+=(double)n/(i+j+k);
}
}
}
}
printf("%.15lf\n",f[a[1]][a[2]][a[3]]);
return 0;
}
- K - Stones
最简单的博弈论递推
从小到大枚举,如果这个点的所有转移点都是必胜点,那么这个点就是比败点,否则就是必胜点
判断一下就可以了
int n=read(),k=read(),dp[M],a[M];
signed main(){
dp[0]=2;
rep(i,1,n,1) a[i]=read();
rep(i,1,k,1){
dp[i]=2;
rep(j,1,n,1){
if(a[j]<=i){
if(dp[i-a[j]]==2) dp[i]=1;
}
}
}
if(dp[k]==1) puts("First");
else puts("Second");
return 0;
}
- L - Deque
最简单的区间dp模板,考虑利用前缀和快速获取一段区间的值
那么会有非常明显的dp方程,考虑取队首队尾的区别
\(f_{l,r}=\max\begin{cases}sum_{l,r}-f_{l+1,r}&\\sum_{l,r}-f_{l,r-1}\end{cases}\)
边界条件就是\(f_{i,i}=a_i\)
答案就是\(f_{1,n}-(sum_{1,n}-f_{1,n})\)
n=read();rep(i,1,n,1)a[i]=read(),dp[i][i]=a[i],sum[i]=sum[i-1]+a[i];
rep(len,2,n,1)
for(int l=1,r=l+len-1;r<=n;l++,r++)
dp[l][r]=max(
sum[r]-sum[l-1]-dp[l+1][r],
sum[r]-sum[l-1]-dp[l][r-1]
); // 进行 dp
cout<<dp[1][n]-(sum[n]-dp[1][n]);
return 0;
}
/
- M - Candies
前缀和优化dp的版子,先考虑最简单的dp方程:
\(f_{i,j}=\sum^{a_i}_{x=0}f_{i-1,j-x}\)
发现每次搞sum累加很麻烦,前缀和优化即可)
int n=read(),k=read(),a[M],dp[105][M],sum[105][M];
signed main(){
rep(i,1,n,1) a[i]=read();
dp[1][0]=sum[1][0]=1;
for(int i=1;i<=k;i++)
dp[1][i]=(i<=a[1]),sum[1][i]=dp[1][i]+sum[1][i-1];
for(int i=2;i<=n;i++){
dp[i][0]=sum[i][0]=1;
for(int j=1;j<=k;j++){
if(j<=a[i])dp[i][j]=sum[i-1][j]%mod;
else dp[i][j]=(sum[i-1][j]-sum[i-1][j-a[i]-1]+mod)%mod;
sum[i][j]=(sum[i][j-1]+dp[i][j])%mod;;
}
}
cout<<dp[n][k]<<endl;
return 0;
}
/*
- N - Slimes
模板,区间dp
方程为\(f_{i,j}=\min{f_{i,j-1}+a_j,f_{i+1,j}+a_i}\)
边界条件为\(f_{i,i}=a_i\)
long long a[400],dp[500][500],sum[10050];
long long n,x;
int main(){
memset(dp,0x3f3f3f3f,sizeof(dp));
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
sum[i]=sum[i-1]+a[i];
dp[i][i]=0;
}
for(int len=1;len<n;len++){
for(int i=1;i+len<=n;i++){
int j=i+len;
for(int k=i;k<j;k++){
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]);
}
}
}
cout<<dp[1][n]<<endl;
return 0;
}
- O - Matching
状压版子,用01序列表示选了那些人,可以配成那些对
int n=read(),vis[30][30],f[30][M],sum[M];
bitset<25>qqt,bbb;
signed main(){
rep(i,1,n,1) rep(j,1,n,1) vis[i][j]=read();
rep(i,0,(1<<n)-1,1){
qqt=i;
sum[i]=qqt.count();
}
f[0][0]=1;//没有用1个女的
rep(i,1,n,1){
rep(s,0,(1<<n)-1,1){
if(sum[s]!=i) continue;//人数不对
qqt=s;//局面
rep(j,1,n,1){//枚举上一个选的是那个女的
if(vis[i][j]==1){
if(qqt.test(j-1)){
bbb=qqt;
bbb.reset(j-1);
f[i][s]=(f[i][s]+f[i-1][bbb.to_ulong()])%mod;
}
}
}
}
}
wr(f[n][(1<<n)-1]);
return 0;
}
- P - Independent Set
简单树形dp版子
用\(f_{u,0/1}\)表示该节点涂黑涂白的方案数,考虑合并子树
分讨子树不同涂色后乘起来就可以了
int n=read(),f[M][2];vector<int>Ed[M];
inline void dfs(int u,int fa){
f[u][0]=f[u][1]=1;
for(auto v:Ed[u]){
if(v==fa) continue;
dfs(v,u);
f[u][0]=(f[u][0]*f[v][1])%mod;//黑
f[u][1]=(f[u][1]*(f[v][1]+f[v][0]))%mod;//白
}
}
signed main(){
rep(i,2,n,1){
int u=read(),v=read();
Ed[u].pb(v);
Ed[v].pb(u);
}
dfs(1,0);
wr((f[1][0]+f[1][1])%mod);
return 0;
}
- Q - Flowers
线段树优化转移dp版子
考虑到每次转移只能从比自己小的节点传过来,考虑权值线段树存f值max
int mx[M<<2];
inline void update(int id,int l,int r,int pos,int val){
if(l==r) return mx[id]=val,void();
int Mid=(l+r)>>1;
if(Mid>=pos) update(ls,l,Mid,pos,val);
else update(rs,Mid+1,r,pos,val);
mx[id]=std::max(mx[ls],mx[rs]);
}
inline int ask(int id,int l,int r,int L,int R){
if(l>=L&&r<=R) return mx[id];
int Mid=(l+r)>>1,maxx=0;
if(Mid>=L) maxx=std::max(maxx,ask(ls,l,Mid,L,R));
if(Mid<R) maxx=std::max(maxx,ask(rs,Mid+1,r,L,R));
return maxx;
}
int n,ans,a[M],f[M],h[M];
signed main(){
n=read();
rep(i,1,n,1) h[i]=read();
rep(i,1,n,1){
a[i]=read();
int maxx=ask(1,1,n,1,h[i]);
f[i]=a[i]+maxx;
ans=std::max(f[i],ans);
update(1,1,n,h[i],f[i]);
}
wr(ans),pr(10);
return 0;
}
- R - Walk
矩阵优化dp版子
每次最短路作floyd,相当于矩阵自乘
考虑重载运算符快速幂即可
struct Mat{
int H,W;
int Sum[55][55];
inline void clear(int h,int w){
H=h;W=w;
rep(i,1,H,1) rep(j,1,W,1) Sum[i][j]=0;
}
inline Mat operator *(const Mat &b)const{
Mat c;
c.clear(H,b.W);
rep(i,1,H,1) rep(k,1,W,1) rep(j,1,b.W,1)
c.Sum[i][j]=(c.Sum[i][j]+Sum[i][k]*b.Sum[k][j])%mod;
return c;
}
}a;
int n,k,ans=0;
inline Mat qp(Mat a,int y){
Mat res;res.clear(a.H,a.W);
rep(i,1,n,1) res.Sum[i][i]=1;
while(y){
if(y&1) res=res*a;
a=a*a;y>>=1;
}return res;
}
signed main(){
n=read();k=read();a.clear(n,n);
rep(i,1,n,1) rep(j,1,n,1) a.Sum[i][j]=read();
a=qp(a,k);
rep(i,1,n,1) rep(j,1,n,1) ans=(ans+a.Sum[i][j])%mod;
wr(ans);
return 0;
}
- S - Digit Sum
数位dp版子,考虑到d很小,所以对取模d作维护
记得记忆化
int f[10005][105],a[10005],D;
inline int dfs(int pos,int sum,bool limit,int res=0){
if(pos==0) return (sum==0)?1:0;
if(!limit&&f[pos][sum]!=-1) return f[pos][sum]%mod;
int up=limit?a[pos]:9;
rep(i,0,up,1)
res=(res+dfs(pos-1,(sum+i)%D,limit&&(i==up)))%mod;
if(!limit) f[pos][sum]=res;
return res;
}
string s;
signed main(){
cin>>s;D=read();
rep(i,1,(int)s.size(),1) a[i]=(int)(s[(int)s.size()-i]-'0');
me(f,-1); wr(((dfs((int)s.size(),0,1)-1)%mod+mod)%mod);
return 0;
}
- T - Permutation
首先考虑最基本的dp转移方程
用 \(f_{i,j}\) 表示在前 \(i\) 个位置上填 \(1 \sim i\) 的数
且最后一个数填了 \(j\) 的方案数.
那么就会有转移方程
\(f_{i,j}\left\{\begin{matrix}\sum^{j-1}_{k=1} f_{i-1,k} && s[i]="< " & \\\sum_{k=i-1}^{j} &&s[i]=">" & \end{matrix}\right.\)
对这个东西作前缀和转移,再滚动一下
可以做到时间\(n^2\),空间\(n\)的优秀复杂度
signed main(){
n=read();f[1]=1;//初始化
rep(i,1,n-1,1){
char ch;cin>>ch;//处理前缀和
rep(j,1,i,1) he[j]=(he[j-1]+f[j]+mod)%mod;
rep(j,1,i+1,1) //如转移方程
if(ch=='<') f[j]=he[j-1];
else f[j]=(he[i]-he[j-1]+mod)%mod;
}//总方案累加
rep(i,1,n,1) ans=(ans+f[i]+mod)%mod;
wr(ans),pr(10);
return 0;
}
- U - Grouping
这个原本是状压,但是模拟退火太爽了,看我模拟退火!
int n,a[20][20],he[20],pos[25];
int ans=-INF;
inline int g(){
int res=0;
rep(i,1,n,1){
rep(j,1,n,1){
if(i!=j&&pos[i]==pos[j])
res+=a[i][j];
}
}
return res/2;
}
inline void solve(){
lb t=1000,eps=1e-15;
rep(i,1,n,1)
pos[i]=Rand()%n+1;
lb cur=g();
while(t>eps){
int pl=Rand()%n+1;
int old=pos[pl];
int new_=Rand()%n+1;
while(new_==old) new_=Rand()%n+1;
pos[pl]=new_;
int now=g();
int E=now-cur;
if(E>0){
cur=now;
}else if(exp(-E/t)*RAND_MAX>rand()){
pos[pl]=old;
}
t*=0.998;
}
ans=max(ans,cur);
}
signed main(){
cin>>n;
if(n==1){
cout<<0<<'\n';
return 0;
}
rep(i,1,n,1) rep(j,1,n,1) cin>>a[i][j];
rep(i,1,100,1) solve();
wr(ans),pr(10);
return 0;
}
- V - Subtree
经典树形dp
考虑计\(f_i\)表示以i为根的子树中出现黑点的次数,\(g_i\)表示子树外选到i的方案数
那么f数组的更新是非常明显的
\(f_u=\prod_{v\in Son_u}^{} (f_v+1)\)
对于子树外的方案,\(g_u=\)
int n,m,qx,f[M],g[M],px[M];vector<int>Ed[M];
inline void Adde(int u,int v){Ed[u].pb(v);}
inline void dfs1(int u,int fa){
f[u]=1;tep(v,Ed[u]){
if(v==fa) continue;
dfs1(v,u);
f[u]=((f[u]*((f[v]+1+m)%m))+m)%m;
}
}
inline void dfs2(int u,int fa){
px[(int)Ed[u].size()]=qx=1;
dep(i,(int)Ed[u].size()-1,0,1){
int v=Ed[u][i];
if(v==fa){px[i]=px[i+1];continue;}
px[i]=((px[i+1]*(f[v]+1))+m)%m;
}
rep(i,0,(int)Ed[u].size()-1,1){
int v=Ed[u][i];
if(v==fa)continue;
g[v]=(g[u]*qx*px[i+1]+1+m)%m;
qx=(qx*(f[v]+1)+m)%m;
}
tep(v,Ed[u]) if(v!=fa) dfs2(v,u);
}
int u,v;
signed main(){
n=read();m=read();g[1]=1;
rep(i,2,n,1)
u=read(),v=read(),Adde(u,v),Adde(v,u);
dfs1(1,0);dfs2(1,0);
rep(i,1,n,1)
wr((f[i]*g[i]+m)%m),pr(10);
return 0;
}

dddddddddpppppppp
浙公网安备 33010602011771号