动态规划指南 QwQ

线性 dp

线性 dp,就是指 dp 只有一维的 dp。因此,线性 dp 容易由 O ⁡ ( n ) \operatorname{O}(n) O(n) 的式子推出来。有时候,线性 dp 是需要结合其他的方法优化或者证明的。

例题

这一道题目可以很明显地推出式子,设 d p i dp_i dpi 为以 i i i 结尾的子序列最大长度。
d p i = max ⁡ j = 1 i − 1 ( d p j [ a i ≥ a j ] + 1 ) \Large dp_i=\max_{j=1}^{i-1}(dp_j[a_i\ge a_j]+1) dpi=j=1maxi1(dpj[aiaj]+1)
那么,接下来,求出:
max ⁡ i = 1 n ( d p i ) \Large\max_{i=1}^{n}(dp_i) i=1maxn(dpi)
即可。

#include<bits/stdc++.h>
#define MAXN 5005
using namespace std;
int n,ans,a[MAXN],dp[MAXN];
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;++i){
		scanf("%d",&a[i]);
	}
	dp[n]=1;
	for(int i=n-1;i>=1;--i){
		int ans=0;
		for(int j=i+1;j<=n;++j){
			if(a[i]<=a[j]&&ans<dp[j]){
				ans=dp[j];
			}
		}
		dp[i]=1+ans;
	}
	for(int i=1;i<=n;++i){
		if(ans<dp[i]){
			ans=dp[i];
		}
	}
	printf("%d",ans);
	return 0;
}

背包 dp

01 背包

01 背包的题型是每一个物品都有价值 w i w_i wi 和容量 v i v_i vi,你有一个总容量 V V V,你要从中选出若干个物品使得能够都装进背包然后价值总和最大。

很容易得出,将 d p i , j dp_{i,j} dpi,j 表示为选到第 i i i 个物品,剩余容量为 j j j 的最大价值。且可以得出:
d p i , j = max ⁡ k = 1 i − 1 ( d p k , j − v i + w i ) \Large dp_{i,j}=\max_{k=1}^{i-1}(dp_{k,j-v_{i}+w_{i}}) dpi,j=k=1maxi1(dpk,jvi+wi)
如果每一个 d p i , j dp_{i,j} dpi,j 都继承上一个 d p i − 1 , j dp_{i-1,j} dpi1,j,那么发现每一个 i i i 都可以化成 i i i i − 1 i-1 i1 的情况,所以可以用 d p 2 , V dp_{2,V} dp2,V 来滚。

例题

01 背包板子题。

#include<bits/stdc++.h>
#define MAXN 101
#define MAXM 1001
using namespace std; 
int w[MAXN],v[MAXN],dp[MAXM];
int main(){
	int m,n;
    scanf("%d %d",&m,&n);
    for(int i=1;i<=n;++i){
        scanf("%d %d",&w[i],&v[i]);
    }
    for(int i=1;i<=n;++i){
        for(int j=m;j>=0;--j){
            if(j>=w[i]){
                dp[j]=max(dp[j-w[i]]+v[i],dp[j]);
            }
        }
    }
    printf("%d",dp[m]);
    return 0;
}

多重背包

多重背包是指每一个物品都能够选择若干次的背包。有时候,可以暴力拆成若干个 01 背包,但是有时候空间和时间都不支持。所以,我们需要优化。

二进制分组优化

这个是利用任何一个数都能够拆成 ∑ 2 s i \sum2_{s_i} 2si 的形式。可以把个数 c i c_i ci 拆成 1 , 2 , 4 … 1,2,4\dots 1,2,4 的形式,这样就能拼凑出所有方案。

单调队列优化

朴素的转移方程是:
d p i , j = max ⁡ k = 0 c i ( d p i − 1 , j − k × v i + k × w i ) \Large dp_{i,j}=\max_{k=0}^{c_i}(dp_{i-1,j-k\times v_i}+k\times w_i) dpi,j=k=0maxci(dpi1,jk×vi+k×wi)
我们掏出一个 G x , y = d p i , x × v i + y G_{x,y}=dp_{i,x\times v_i+y} Gx,y=dpi,x×vi+y,一个 H x , y = d p i − 1 , x × v i + y H_{x,y}=dp_{i-1,x\times v_i+y} Hx,y=dpi1,x×vi+y x x x 表示选多少个, y y y 表示剩余容量,则有:
G x , y = max ⁡ k = 0 c i ( H x − k , y + k × w i ) \Large G_{x,y}=\max_{k=0}^{c_i}(H_{x-k,y}+k\times w_i) Gx,y=k=0maxci(Hxk,y+k×wi)
我们再掏出一个 S x , y = H x , y − x × w i S_{x,y}=H_{x,y}-x\times w_i Sx,y=Hx,yx×wi,则有:
G x , y = max ⁡ k = 0 c i ( S x − k , y ) + x × w i \Large G_{x,y}=\max_{k=0}^{c_i}(S_{x-k,y})+x\times w_i Gx,y=k=0maxci(Sxk,y)+x×wi
很明显可以使用单调队列进行维护。

例题

套用单调队列优化背包模板。

#include<bits/stdc++.h>
#define MAXN 40004
using namespace std; 
int dp[MAXN];
int main(){
	int n,m,ans=0;
	scanf("%d %d",&n,&m);
	while(n--){
		int w,v,sum;
		scanf("%d %d %d",&v,&w,&sum);
		if(!w){
			ans+=v*sum;
			continue;
		}
		sum=min(sum,m/w);
		for(int i=0;i<w;++i){
			deque<pair<int,int> > q;
			int part=(m-i)/w;
			for(int j=0;j<=part;++j){
				while(!q.empty()&&dp[i+j*w]-j*v>=q.back().second){
					q.pop_back();
				}
				q.push_back(make_pair(j,dp[i+j*w]-j*v));
				while(!q.empty()&&q.front().first<j-sum){
					q.pop_front();
				}
				dp[i+j*w]=max(dp[i+j*w],q.front().second+j*v);
			}
		}
	}
	printf("%d",ans+dp[m]);
	return 0;
}

无限背包

无限背包是指各种背包都有无限个,很明显可以有暴力式子:
d p i , j = max ⁡ k = 0 i − 1 ( d p k , j − v i × k + w i × k ) \Large dp_{i,j}=\max_{k=0}^{i-1}(dp_{k,j-v_i\times k}+w_i\times k) dpi,j=k=0maxi1(dpk,jvi×k+wi×k)
但是,可以发现, d p i , j dp_{i,j} dpi,j 只需要通过 d p i − 1 , j − v i dp_{i-1,j-v_i} dpi1,jvi 来转移即可。所以,基本上变成了 01 背包的代码。同样可以优化掉一维来优化。

例题

套用无限背包板子。

#include<bits/stdc++.h>
#define MAXN 10001
#define MAXM 10000001
using namespace std;
typedef long long ll;
ll v[MAXN],w[MAXN],dp[MAXM];
int main(){
	int m,n;
	scanf("%d %d",&m,&n);
	for(int i=1;i<=n;++i){
	    scanf("%lld %lld",&w[i],&v[i]);
	}
	for(int i=1;i<=n;++i){
		for(int j=w[i];j<=m;++j){
			dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
		}
	}
	printf("%lld",dp[m]);
	return 0;
}

混合背包

混合背包是指有一些物品能够拿 1 1 1 次,有些物品能够拿 c i c_i ci 次,有些物品能够拿无限次。这种背包只需要分类讨论每一种背包即可。

例题

套用混合背包板子。

#include<bits/stdc++.h>
#define MAXN 10001
#define MAXM 1001
using namespace std;
typedef long long ll;
int w[MAXN],v[MAXN],cnt[MAXN];
ll dp[MAXM];
inline ll time(){
	ll sh,ss,eh,es;
	scanf("%lld:%lld %lld:%lld",&sh,&ss,&eh,&es);
	ll s=sh*60+ss,e=eh*60+es;
	return e-s;
}
int main(){
	int m=time(),n;
	scanf("%d",&n);
	for(int i=1;i<=n;++i){
		scanf("%d %d %d",&v[i],&w[i],&cnt[i]);
	}
	for(int i=1;i<=n;++i){
		if(!cnt[i]){
			for(int j=v[i];j<=m;++j){
				dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
			}
		}else{
			for(int j=1;j<=cnt[i];++j){
				for(int k=m;k>=j*v[i];--k){
					dp[k]=max(dp[k],dp[k-v[i]]+w[i]);
				}
			}
		}
	}
	printf("%lld",dp[m]);
	return 0;
} 

区间 dp

区间 dp 是指在 d p l , r dp_{l,r} dpl,r 形式的 dp。这种 dp 是通过子区间来进行合并转移的,即 d p l , r = m e r g e ( d p l , k , d p k + 1 , r ) dp_{l,r}=merge(dp_{l,k},dp_{k+1,r}) dpl,r=merge(dpl,k,dpk+1,r)。这种思想是基于分治的。比如要想得到 d p 1 , 4 dp_{1,4} dp1,4,就得先得到 d p 1 , 1 dp_{1,1} dp1,1 d p 2 , 4 dp_{2,4} dp2,4 d p 1 , 2 dp_{1,2} dp1,2 d p 3 , 4 dp_{3,4} dp3,4 或者 d p 1 , 3 dp_{1,3} dp1,3 d p 4 , 4 dp_{4,4} dp4,4

例题

d p l , r dp_{l,r} dpl,r 为合并 [ l , r ] [l,r] [l,r] 的最大值,然后可以套用 dp 公式。

#include<bits/stdc++.h>
#define MAXN 202
using namespace std;
int a[MAXN],dp[MAXN][MAXN];
int main(){
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;++i){
		scanf("%d",&a[i]);
		a[i+n]=a[i];
	}
	for(int len=2;len<=2*n;++len){
		for(int l=1,r=len;r<=2*n;++l,++r){
			for(int k=l;k<r;++k){
				dp[l][r]=max(dp[l][r],dp[l][k]+dp[k+1][r]+a[l]*a[r+1]*a[k+1]);
			}
		}
	}
	int ans=0;
	for(int i=1;i<=2*n;++i){
		ans=max(ans,dp[i][i+n-1]);
	}
	printf("%d",ans);
	return 0;
}

树形 dp

树形 dp 是通过树的顺序或者图的拓扑序来进行的 dp。通常来讲,子节点继承父节点的状态或者父节点继承诸多子节点的状态。

例题

很明显,看到这一条保证:

如果有一个编号为 A A A 机器人指控或保护编号为 B B B 的机器人,那么我们保证 A < B A<B A<B

就可以推出这一个关系是一棵树。这时候,我们就可以使用树形 dp 来解。

很明显,当一条指控边出现,只可能是:

  • 狼人指控村民。
  • 村民指控狼人。
  • 村民指控村民。

当一条保护边出现,只可能是:

  • 狼人保护狼人。
  • 村民保护狼人。
  • 村民保护村民。

所以,利用这两点来建边就可以进行 dp 了。

具体可以使用记忆化搜索来实现。由于可能有森林的出现,所以定义一个根节点 0 0 0 来连接其他点。从 0 0 0 开始搜索,向下搜索,设 d p i , j , k ( 1 ≤ i ≤ n , 1 ≤ j ≤ m , k ∈ { 0 , 1 } ) dp_{i,j,k}(1\le i\le n,1\le j\le m,k\in\{0,1\}) dpi,j,k(1in,1jm,k{0,1}) 为当前是节点 i i i,前面出现了 j j j 个狼人,当前节点是否为狼人的方案数。

接下来,设 u , v u,v u,v 表示 u u u 有一条边通向 v v v,那么分类讨论:

  • 如果要计算为狼人的情况,并且为指控边,那么子节点必须为村民,则有 d p u , j , 1 = d p u , j − k , 1 × d p v , k , 0 dp_{u,j,1}=dp_{u,j-k,1}\times dp_{v,k,0} dpu,j,1=dpu,jk,1×dpv,k,0
  • 如果要计算为狼人的情况,并且为保护边,那么子节点必须为狼人,则有 d p u , j , 1 = d p u , j − k , 1 × d p v , k , 1 dp_{u,j,1}=dp_{u,j-k,1}\times dp_{v,k,1} dpu,j,1=dpu,jk,1×dpv,k,1
  • 如果要计算为村民的情况,并且为指控边,那么子节点可以为村民和狼人,需要加和,有 d p u , j , 0 = d p u , j − k , 0 × ( d p v , k , 0 + d p v , k , 1 ) dp_{u,j,0}=dp_{u,j-k,0}\times(dp_{v,k,0}+dp_{v,k,1}) dpu,j,0=dpu,jk,0×(dpv,k,0+dpv,k,1)
  • 如果要计算为村民的情况,并且为保护边,那么子节点可以为村民和狼人,需要加和,有 d p u , j , 0 = d p u , j − k , 0 × ( d p v , k , 0 + d p v , k , 1 ) dp_{u,j,0}=dp_{u,j-k,0}\times(dp_{v,k,0}+dp_{v,k,1}) dpu,j,0=dpu,jk,0×(dpv,k,0+dpv,k,1)
#include<bits/stdc++.h>
#define MAXN 202
#define MAXM 40004
#define MOD 1000000007
using namespace std;
typedef long long ll; 
struct node{
	int next,to;
	bool type;
}edge[MAXM<<2];
int n,m,k,cnt,head[MAXN];
bool vis[MAXN];
inline void addedge(int from,int to,bool type){
	edge[++cnt].to=to;
	edge[cnt].type=type;
	edge[cnt].next=head[from];
	head[from]=cnt;
} 
ll dp[MAXN][MAXN][2];
int size[MAXN];
bool in[MAXN];
void dfs(int u){
	size[u]=1;
	dp[u][0][0]=dp[u][1][1]=1ll;
	for(int i=head[u];i;i=edge[i].next){
		int v=edge[i].to;
		dfs(v);
		size[u]+=size[v];
		for(int j=size[u];j>=0;--j){
			ll ans1=0,ans0=0;
			for(int k=min(j,size[v]);k>=0;--k){
				if(edge[i].type){
					(ans1+=dp[u][j-k][1]*dp[v][k][0])%=MOD;
				}else{
					(ans1+=dp[u][j-k][1]*dp[v][k][1])%=MOD;
				}
	 			(ans0+=dp[u][j-k][0]*(dp[v][k][0]+dp[v][k][1]))%=MOD;
			}
			dp[u][j][0]=ans0;
			dp[u][j][1]=ans1;
		}
	}
}
int main(){
	scanf("%d %d %d",&n,&m,&k);
	while(k--){
		int u,v;
		char s[3]="";
		scanf("%s %d %d",s,&u,&v);
		if(*s=='A'){
			addedge(u,v,true);
		}else{
			addedge(u,v,false);
		}
		in[v]=true;
	}
	for(int i=1;i<=n;++i){
		if(!in[i]){
			addedge(0,i,true);
		}
	} 
	dfs(0);
	printf("%lld",dp[0][m][0]);
	return 0;
}

状压 dp

状压 dp,就是把一些维度压缩成一个维度的 dp。比如,你需要记录 1 1 1 10 10 10 之内的布尔值,那么可以把 10 10 10 位压缩成一个 int,也就是 2 10 2^{10} 210 的空间。

有一些状压的基本操作:

  • (zip>>i)&1表示查询 z i p zip zip 单位里面的第 i i i 个元素的布尔值。
  • zip|(1<<(i-1))表示在 z i p zip zip 的基础上将第 i i i 个元素值修改成 1 1 1
  • zip-(1<<(i-1))表示在 z i p zip zip 的第 i i i 的位置上是 1 1 1 的基础上将 i i i 修改成 0 0 0
  • __builtin_popcount(zip)表示统计 z i p zip zip 中有多少个 1 1 1

状态压缩通常用于优化数位 dp。

例题

d p z i p , i dp_{zip,i} dpzip,i 表示走过的状态为 z i p zip zip,下一个要走第 i i i 个的状态。枚举每一个 j j j 转移过来。如果 z i p zip zip 里面出现过 j j j,那就更新答案。

#include<bits/stdc++.h>
#define MAXN 21
using namespace std;
int n,dis[MAXN][MAXN],dp[1<<20|1][MAXN];
inline int read(){
    int res=0;
    char c=getchar();
    while(!isdigit(c)){
        c=getchar();
    }
    while(isdigit(c)){
        res=(res<<1)+(res<<3)+(c^48);
        c=getchar();
    }
    return res;
}
inline void write(int x){
    if(x>9){
        write(x/10);
    }
    putchar(x%10+'0');
}
int main(){
    n=read();
    for(register int i=1;i<=n;++i){
    	for(register int j=1;j<=n;++j){
    		dis[i][j]=read();
		}
	}
    memset(dp,0x3f,sizeof(dp));
	dp[1][1]=0;
    for(register int zip=1;zip<(1<<n);++zip){
    	for(register int i=1;i<=n;++i){
    		if(zip&(1<<(i-1))){
    			continue;
			}
			for(register int j=1;j<=n;++j){
				if(!(zip&(1<<(j-1)))){
					continue;
				}
				dp[zip|(1<<(i-1))][i]=min(dp[zip|(1<<(i-1))][i],dp[zip][j]+dis[j][i]);
			}
		}
	}
    int ans=INT_MAX;
    for(register int i=2;i<=n;++i){
    	ans=min(ans,dp[(1<<n)-1][i]+dis[i][1]);
	}
    write(ans);
    return 0;
}

优化

单调队列优化

在背包 dp 里面有提到过。原理是将 d p dp dp 式子化成形如 max ⁡ i = 0 n ( d p ) \max_{i=0}^{n}(dp) maxi=0n(dp) 类型的式子。

例题

很明显,初始 dp 式子是 d p t , i , j = max ⁡ ( d p t − 1 , i , j , d p t − 1 , p r e ( i ) , p r e ( j ) ) dp_{t,i,j}=\max(dp_{t-1,i,j},dp_{t-1,pre(i),pre(j)}) dpt,i,j=max(dpt1,i,j,dpt1,pre(i),pre(j))。其中, p r e pre pre 函数是该位置的上一个位置。在同一个方向上,可以使用单调队列优化。

#include<bits/stdc++.h>
#define MAXN 210
using namespace std;
int n,m,x,y,k,ans;
int dx[5]={0,-1,1,0,0};
int dy[5]={0,0,0,-1,1};
int dp[MAXN][MAXN];
char mp[MAXN][MAXN];
inline void move(int x,int y,int p,int l,int r){
	deque<pair<int,int> > q;
	for(int i=1;1<=x&&x<=n&&1<=y&&y<=m;++i,x+=dx[p],y+=dy[p]){
		if(mp[x][y]=='x'){
			q.clear();
		}else{
			while(!q.empty()&&q.back().first+i-q.back().second<dp[x][y]){
				q.pop_back();
			}
			q.push_back(make_pair(dp[x][y],i));
			while(!q.empty()&&q.back().second-q.front().second>r-l+1){
				q.pop_front();
			}
			ans=max(ans,dp[x][y]=q.front().first+i-q.front().second);
		}
	}
}
int main(){
	scanf("%d %d %d %d %d",&n,&m,&x,&y,&k);
	for(int i=1;i<=n;++i){
		scanf("%s",mp[i]+1);
	}
	memset(dp,0xf3,sizeof(dp));
	dp[x][y]=0;
	for(int i=1;i<=k;++i){
		int l,r,face; 
		scanf("%d %d %d",&l,&r,&face);
		if(face==1){
			for(int j=1;j<=m;++j){
				move(n,j,face,l,r); 
			}
		}else if(face==2){
			for(int j=1;j<=m;++j){
				move(1,j,face,l,r);
			}
		}else if(face==3){
			for(int j=1;j<=n;++j){
				move(j,m,face,l,r);
			}
		}else if(face==4){
			for(int j=1;j<=n;++j){
				move(j,1,face,l,r);
			}
		}
	}
	printf("%d",ans);
	return 0;
}

矩阵快速幂

矩阵快速幂是用于优化 dp 的。它通常能够优化到 log ⁡ \log log 级别的递推。

矩阵快速幂的本质是利用矩阵乘法的乘法结合律来用 log ⁡ n \log n logn 的时间复杂度来求出分矩阵,然后乘上原矩阵。

矩阵乘法的规则如下:

  • 定义 x x x 为长度 n , m n,m n,m 的矩阵, y y y 为长度 m , k m,k m,k 的矩阵,那么它们相乘的结果为长度为 n , k n,k n,k 的矩阵 r e s res res
  • r e s i , j = ∑ k = 1 m x i , k × y k , j res_{i,j}=\sum_{k=1}^{m}x_{i,k}\times y_{k,j} resi,j=k=1mxi,k×yk,j

可以看出这一个式子似乎能够优化所有转移方程只带加法和乘法的 dp。但是,要证明两点:

  • 矩阵乘法不满足乘法交换律,因为交换后 n n n 可能不等于 k k k
  • 矩阵乘法满足结合律。因为 { n , m } × { m , k } × { k , l } \{n,m\}\times\{m,k\}\times\{k,l\} {n,m}×{m,k}×{k,l},不管先算哪一个,要么得出 { n , k } × { k , l } = { n , l } \{n,k\}\times\{k,l\}=\{n,l\} {n,k}×{k,l}={n,l},要么得出 { n , m } × { m , l } = { n , l } \{n,m\}\times\{m,l\}=\{n,l\} {n,m}×{m,l}={n,l}

所以,矩阵乘法满足结合律。那么,矩阵乘法的递推式要视情况而定。

例题

这一道题目可以推出 dp 式子,设 d p i , k ( 1 ≤ i ≤ n , k ∈ { 0 , 1 } ) dp_{i,k}(1\le i\le n,k\in\{0,1\}) dpi,k(1in,k{0,1}) 表示表示所有以 i i i 为结尾的子串,选择相应的 b b b,满足 k k k 对于相应子串是忠诚的,且 k k k 的结尾与相应子串相同和不同的概率和。

d p i , 0 / 1 dp_{i,0/1} dpi,0/1 表示所有以 i i i 结尾的且长度在 l l l r r r 之间的字串,选择相应的 j j j,满足 j j j 对于相应字串是忠诚的,且 j j j 的结尾相应字串相同和不同的概率和。另外设置辅助 dp 数组 l d p i , 0 / 1 ldp_{i,0/1} ldpi,0/1 r d p i , 0 / 1 rdp_{i,0/1} rdpi,0/1 表示 d p dp dp 数组,不同的是长度分别为 1 1 1 l − 1 l-1 l1 r + 1 r+1 r+1 n n n。递推式:
d p i , 0 = 1 2 × ( d p i − 1 , 0 + d p i − 1 , 1 ) + l d p i , 0 2 l − r d p i , 0 2 r \Large dp_{i,0}=\frac{1}{2}\times(dp_{i-1,0}+dp_{i-1,1})+\frac{ldp_{i,0}}{2^l}-\frac{rdp_{i,0}}{2^r} dpi,0=21×(dpi1,0+dpi1,1)+2lldpi,02rrdpi,0

g i , 0 / 1 g_{i,0/1} gi,0/1 表示到第 i i i 个位置有多少个字串 j j j 是忠诚的,有递推式 g i , 0 = g i − 1 , 0 + g i − 1 , 1 g_{i,0}=g_{i-1,0}+g_{i-1,1} gi,0=gi1,0+gi1,1 g i , 1 = g i − 1 , a i g_{i,1}=g_{i-1,a_i} gi,1=gi1,ai。那么这一部分可以使用矩阵快速幂优化。之后,可以利用 g g g 来求出 l d p ldp ldp r d p rdp rdp

#include<bits/stdc++.h>
#define MOD 998244353
#define MAXN 5000005
#define getchar getchar_unlocked
#define putchar putchar_unlocked
using namespace std;
typedef long long ll;
typedef vector<vector<ll> > matrix;
matrix mtr1[2],mtr2[2],mtr3;
int n,l,r;
ll res1,res2,invl,invr,ans[MAXN],ldp[2][MAXN],rdp[2][MAXN],dp[2][MAXN];
inline ll read(void){
	ll res=0;
	char c=getchar();
	while(!isdigit(c)){
		c=getchar();
	}
	while(isdigit(c)){
		res=(res<<1)+(res<<3)+(c^'0');
		c=getchar();
	}
	return res;
}
void write(ll res){
	if(res>9){
		write(res/10);
	}
	putchar(res%10+'0');
}
inline matrix multi(const matrix &mtr1,const matrix &mtr2){
	matrix res;
	res.resize(2);
	res[0].resize(2);
	res[1].resize(2);
	res[0][0]=(mtr1[0][0]*mtr2[0][0]+mtr1[0][1]*mtr2[1][0])%MOD;
	res[1][0]=(mtr1[1][0]*mtr2[0][0]+mtr1[1][1]*mtr2[1][0])%MOD;
	res[0][1]=(mtr1[0][0]*mtr2[0][1]+mtr1[0][1]*mtr2[1][1])%MOD;
	res[1][1]=(mtr1[1][0]*mtr2[0][1]+mtr1[1][1]*mtr2[1][1])%MOD;
	return res;
}
inline ll power(ll x,ll y){
	ll res=1;
	while(y){
		if(y&1){
			(res*=x)%=MOD;
		};
		(x*=x)%=MOD;
		y>>=1;
	}
	return res;
}
inline ll inv(int number){
	return power(number,MOD-2);
}
const static ll invof2=inv(2);
int main(){
	mtr1[0].resize(2);
	mtr1[1].resize(2);
	mtr2[0].resize(2);
	mtr2[1].resize(2);
	for(register int i=0;i<2;++i){
		mtr1[0][i].resize(2);
		mtr1[1][i].resize(2);
		mtr2[0][i].resize(2);
		mtr2[1][i].resize(2);
	}
	mtr1[0][0][0]=mtr1[0][0][1]=mtr1[0][1][0]=1;
	mtr2[0][0][1]=mtr2[0][1][0]=1;
	mtr1[1][0][0]=mtr1[1][1][0]=mtr1[1][1][1]=1;
	mtr2[1][0][0]=mtr2[1][1][1]=1;
	mtr2[0][1][1]=mtr2[1][1][0]=MOD-1;
	n=read();
	l=read();
	r=read();
	invl=inv(power(2,l));
	invr=inv(power(2,r+1));
	res2=inv((1ll*(n*2-l-r+2)*(r-l+1)/2)%MOD);
	for(register int i=1;i<=n;++i){
		char c=getchar();
		while(c!='1'&&c!='0'){
			c=getchar();
		}
		ans[i]=c-'0';
	}
	mtr3.resize(2);
	mtr3[0].resize(2);
	mtr3[1].resize(2);
	mtr3[0][0]=mtr3[1][1]=1;
	mtr3[0][1]=mtr3[1][0]=0;
	for(register int i=1;i<l;++i){
		mtr3=multi(mtr3,mtr1[ans[i]]);
	}
	for(register int i=l;i<=n;++i){
		mtr3=multi(multi(mtr2[ans[i-l+1]],mtr3),mtr1[ans[i]]);
		ldp[0][i]=(mtr3[0][0]+mtr3[1][0])%MOD;
		ldp[1][i]=(mtr3[0][1]+mtr3[1][1])%MOD;
	}
	mtr3[0][0]=mtr3[1][1]=1;
	mtr3[0][1]=mtr3[1][0]=0;
	for(register int i=1;i<=r;++i){
		mtr3=multi(mtr3,mtr1[ans[i]]);
	}
	for(register int i=r+1;i<=n;++i){
		mtr3=multi(multi(mtr2[ans[i-r]],mtr3),mtr1[ans[i]]);
		rdp[0][i]=(mtr3[0][0]+mtr3[1][0])%MOD;
		rdp[1][i]=(mtr3[0][1]+mtr3[1][1])%MOD;
	}
	for(register int i=1;i<=n;++i){
		(dp[0][i]=(invof2*(dp[0][i-1]+dp[1][i-1])+invl*ldp[0][i]-invr*rdp[0][i])%MOD+MOD)%=MOD;
		(dp[1][i]=(invof2*(dp[ans[i]][i-1])+invl*ldp[1][i]-invr*rdp[1][i])%MOD+MOD)%=MOD;
		(res1+=(dp[0][i]+dp[1][i])%MOD)%=MOD;
	}
	write((res1*res2%MOD+MOD)%MOD);
	return 0;
}

四边形不等式

如果 dp 式子是求形如以下递推式:
d p i = min ⁡ j = 0 i − 1 ( w ( j , i ) ) \Large dp_{i}=\min_{j=0}^{i-1}(w(j,i)) dpi=j=0mini1(w(j,i))
并且满足决策单调性 [ i < j ] = [ o p t ( i ) ≤ o p t ( j ) ] [i<j]=[opt(i)\le opt(j)] [i<j]=[opt(i)opt(j)],那么可以使用四边形不等式: [ a ≤ b ≤ c ≤ d ] = [ w ( a , c ) + w ( b , d ) ≥ w ( a , d ) + w ( c , d ) ] [a\le b\le c\le d]=[w(a,c)+w(b,d)\ge w(a,d)+w(c,d)] [abcd]=[w(a,c)+w(b,d)w(a,d)+w(c,d)]。利用这个性质,可以把 O ⁡ ( n 2 ) \operatorname{O}(n^2) O(n2) 优化到 O ⁡ ( n log ⁡ n ) \operatorname{O}(n\log n) O(nlogn)

cdq 分治优化

对于每一个 o p t ( i ) opt(i) opt(i),先计算前半段: [ 1 , ⌊ n 2 ⌋ ] [1,\lfloor\frac{n}{2}\rfloor] [1,2n⌋],再计算后半段 [ ⌊ n 2 ⌋ + 1 , n ] [\lfloor\frac{n}{2}\rfloor+1,n] [⌊2n+1,n],对于每一个 c d q ( l , r ) cdq(l,r) cdq(l,r) 的最优决策点都会处理到,并且可以合并答案,类似于区间 dp,可以优化到 O ⁡ ( n log ⁡ n ) \operatorname{O}(n\log n) O(nlogn)。下面给出伪代码:

inline int w(int i,int j);//代价函数
void cdq(int l,int r,int kl,int kr){
	int mid=(l+r)>>1,k=kl;
	for(int i=kl;i<=min(kr,mid-1);++i){
		if(w(i,mid)<w(k,mid)){
			k=j;//如果满足不等式 
		}
		dp[mid]=w(k,mid);//得到答案 
	}
	if(l<mid){
		cdq(l,mid-1,kl,k);//向左搜 
	}else{
		cdq(mid+1,r,k,kr);//向右搜 
	}
}
例题

很明显的决策单调性, w ( i , j + 1 ) + w ( i + 1 , j ) ≥ w ( i , j ) + w ( i + 1 , j + 1 ) w(i,j+1)+w(i+1,j)\ge w(i,j)+w(i+1,j+1) w(i,j+1)+w(i+1,j)w(i,j)+w(i+1,j+1),所以套用四边形不等式模板即可。

#include<bits/stdc++.h>
#define MAXN 500005
using namespace std;
int a[MAXN];
double sqr[MAXN],dp[MAXN];
inline double w(int i,int j){
	return double(a[i])+sqr[abs(i-j)];
}
void cdq(int l,int r,int kl,int kr){
	if(l>r){
		return;
	}
	int mid=(l+r)>>1,k;
	double ans=0;
	for(int i=kl;i<=min(mid,kr);++i){
		if(w(i,mid)>ans){
			ans=w(i,mid);
			k=i;
		}
	}
	dp[mid]=max(dp[mid],ans);
	cdq(l,mid-1,kl,k);
	cdq(mid+1,r,k,kr);
}
int main(){
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;++i){
		scanf("%d",&a[i]);
		sqr[i]=sqrt(i);
	}
	cdq(1,n,1,n);
	reverse(a+1,a+1+n);
	reverse(dp+1,dp+1+n);
	cdq(1,n,1,n);
	for(int i=n;i>=1;--i){
		printf("%d\n",int(ceil(dp[i]))-a[i]);
	}
	return 0;
}
```v
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值