AtCoder DP Contest 题目全讲(下)

前言:

洛谷题单(非本人整理)

本篇文章为下期(收录题目 O–>Z \text{O-->Z} O–>Z),题目比较有思维难度,值得一做。


O题

观察题目数据范围,允许状态压缩。

f i , s f_{i,s} fi,s 表示右部中的点,与左部中 i i i 个点构成匹配的集合为 s s s 的方案数,答案为 f n , ( 1 < < n ) − 1 f_{n,(1<<n)-1} fn,(1<<n)1

考虑如何转移,当构成匹配集合为 s s s 时,我们发现二进制表示下 1 1 1 的数量(即当前匹配数),应当等于 i i i,这样才能更新。

找到一个右部的一个点,满足它不属于集合 s s s,并且能与当前枚举的左部点匹配,这样就可以进行更新。

#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
	return x*f;
}
const int N=22,M=(1<<N),mod=1e9+7;
int n,a[N][N],f[M];
int popcount(int x){int res=0;while(x){if(x&1)res++;x>>=1;}return res;}
int main(){
	n=rd;FOR(i,0,n-1) FOR(j,0,n-1) a[i][j]=rd;
	f[0]=1;
	FOR(s,0,(1<<n)-1){
		int cnt=popcount(s);
		FOR(i,0,n-1) if(a[cnt][i]&&(s&(1<<i))==0) f[s|(1<<i)]=(1ll*f[s|(1<<i)]+f[s])%mod;
	}
	printf("%d\n",f[(1<<n)-1]);
	return 0;
}

P题

基础树形 dp \text{dp} dp

f x , 0 / 1 f_{x,0/1} fx,0/1 表示以 x x x 为根的子树中, x x x 号节点染白 / / /黑色的方案数。

设边 ( x , y ) (x,y) (x,y),为满足相邻两点不全为黑色的限制,转移为:

  • f x , 0 = f x , 0 × ( f y , 0 + f y , 1 ) f_{x,0}=f_{x,0}\times(f_{y,0}+f_{y,1}) fx,0=fx,0×(fy,0+fy,1)
  • f x , 1 = f x , 1 × f y , 0 f_{x,1}=f_{x,1}\times f_{y,0} fx,1=fx,1×fy,0

1 1 1 为根,答案为 f 1 , 0 + f 1 , 1 f_{1,0}+f_{1,1} f1,0+f1,1

#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
	return x*f;
}
const int N=1e5+10,mod=1e9+7;
int n,head[N],tot,f[N][2];
struct node{int to,nxt;}edge[N<<1];
void add(int x,int y){edge[++tot].to=y,edge[tot].nxt=head[x],head[x]=tot;}
void dfs(int x,int fa){
	f[x][0]=f[x][1]=1;
	for(int i=head[x];i;i=edge[i].nxt){
		int y=edge[i].to;if(y==fa) continue;
		dfs(y,x);
		f[x][0]=1ll*f[x][0]*(f[y][0]+f[y][1])%mod;
		f[x][1]=1ll*f[x][1]*f[y][0]%mod;
	}
}
int main(){
	n=rd;
	FOR(i,1,n-1){int x=rd,y=rd;add(x,y),add(y,x);}
	dfs(1,0);
	printf("%d\n",(1ll*f[1][0]+f[1][1])%mod);
	return 0;
}

Q题

状态设计和转移方程是很显然的,设 f i f_{i} fi 表示前 i i i 朵花,选了第 i i i 朵花能得到的最大权值。

转移为 f i = max ⁡ j = 0 i − 1 { f j } + a i , h j < h i f_{i}=\max_{j=0}^{i-1}\{f_j\}+a_i,h_j<h_i fi=maxj=0i1{fj}+ai,hj<hi

暴力做是 O ( n 2 ) O(n^2) O(n2) 的,考虑优化。

我们发现每次查找的是前面所有满足 h j < h i h_j<h_i hj<hi f j f_j fj 的最大值,所以可以以 h h h 作为下标,建立权值线段树,维护区间中 f f f 最大值。每次查找的都是一段前缀,计算完后还要更新 f i f_i fi

时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)

#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
	return x*f;
}
const int N=2e5+10;
int n,h[N],a[N];ll mx[N<<2];
void pushup(int u){mx[u]=max(mx[u<<1],mx[u<<1|1]);}
void modify(int u,int l,int r,int x,ll v){
	if(l==r){mx[u]=v;return;}
	int mid=(l+r)>>1;
	if(x<=mid) modify(u<<1,l,mid,x,v);
	else modify(u<<1|1,mid+1,r,x,v);
	pushup(u);
}
ll query(int u,int l,int r,int ql,int qr){
	if(ql<=l&&r<=qr) return mx[u];
	int mid=(l+r)>>1;ll ans=0;
	if(ql<=mid) ans=max(ans,query(u<<1,l,mid,ql,qr));
	if(qr>mid) ans=max(ans,query(u<<1|1,mid+1,r,ql,qr));
	return ans;
}
int main(){
	n=rd;ll ans=0;
	FOR(i,1,n) h[i]=rd;
	FOR(i,1,n) a[i]=rd;
	FOR(i,1,n){
		ll tmp=query(1,1,n,1,h[i])+a[i];
		ans=max(ans,tmp),modify(1,1,n,h[i],tmp);
	}
	printf("%lld\n",ans);
	return 0;
}

R题

矩阵加速 dp \text{dp} dp

比较 naive \text{naive} naive dp \text{dp} dp 很好想,设 f t , i , j f_{t,i,j} ft,i,j 表示从 i → j i\to j ij,经过路径长度为 t t t 的方案数,我们可以枚举中间某个点 k k k,则 f t , i , j = ∑ k = 1 n f t − 1 , i , k × f 1 , k , j f_{t,i,j}=\sum_{k=1}^{n}f_{t-1,i,k}\times f_{1,k,j} ft,i,j=k=1nft1,i,k×f1,k,j

我们发现这和矩阵乘法的式子一模一样,而 f 1 f_1 f1 矩阵即为题中所给,而 f t = f t − 1 × f 1 f_t=f_{t-1}\times f_1 ft=ft1×f1,所以最终的答案矩阵为 f k = f 1 k f_k=f_1^k fk=f1k

最后就是一个矩阵加速幂的模版了,不会的可以学一学。

时间复杂度 O ( n 3 l o g k ) O(n^3logk) O(n3logk)

#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
	return x*f;
}
const int N=55,mod=1e9+7;
int n;ll K;
struct matrix{
	int a[N][N];
	matrix(){memset(a,0,sizeof(a));}
	matrix operator*(const matrix&T){
		matrix res;
		FOR(i,0,n-1){
			FOR(k,0,n-1){
				int r=a[i][k];
				FOR(j,0,n-1)
					res.a[i][j]=(1ll*res.a[i][j]+1ll*r*T.a[k][j]%mod)%mod;
			}
		}
		return res;
	}
}M,ans;
int main(){
	n=rd,scanf("%lld",&K);
	FOR(i,0,n-1) ans.a[i][i]=1;
	FOR(i,0,n-1) FOR(j,0,n-1) M.a[i][j]=rd;
	while(K){
		if(K&1) ans=ans*M;
		M=M*M,K>>=1;
	}
	int res=0;
	FOR(i,0,n-1) FOR(j,0,n-1) res=(1ll*res+ans.a[i][j])%mod;
	cout<<res<<endl; 
	return 0;
}

S题

蛮套路的数位 dp \text{dp} dp

我们考虑记忆化搜索来写,从高位到低位枚举,记录当前所有数码和模 D D D 的值,注意判断是否有前导零。
然后写法就和其他记搜数位 dp \text{dp} dp 一样,看代码就能理解了。

#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
	return x*f;
}
const int N=10010,M=110,mod=1e9+7;
int n,cnt,f[N][M][2],d,num[N];char ch[N];
int dfs(int pos,int sum,bool zero,bool lim){
	if(!pos) return sum==0&&!zero;//枚举完了
	if(!lim&&~f[pos][sum][zero]) return f[pos][sum][zero];//记搜
	int up=lim?num[pos]:9,res=0;//是否有最高位的限制
	FOR(i,0,up) res=(1ll*res+dfs(pos-1,(sum+i)%d,zero&&i==0,lim&&i==up))%mod;
	if(!lim) f[pos][sum][zero]=res;
	return res;
}
int main(){
	scanf("%s",ch+1),n=strlen(ch+1),d=rd;
	ROF(i,n,1) num[++cnt]=ch[i]-'0';
	memset(f,-1,sizeof(f)),printf("%d\n",dfs(cnt,0,1,1));
	return 0;
}

T题

这个题非常的妙!

一开始很容易想到 f i , j f_{i,j} fi,j 表示前 i i i 个数,第 i i i 位填 j j j 的方案数,然后 j j j 1 1 1 n n n 枚举,你会发现答案错了。为什么?因为你很容易把不合法的方案给统计上,会算重。

那么怎么做,我们发现其实只关心大小关系,比如 4 , 5 , 6 4,5,6 4,5,6,完全可以映射为 1 , 2 , 3 1,2,3 1,2,3,所以我们规定前 i i i 个数中,就只从 1 → i 1\to i 1i 中添数。所以 j j j 1 → i 1\to i 1i 枚举,对于转移:

  • f i , j = ∑ k = 1 j − 1 f i − 1 , k , (if op=<) f_{i,j}=\sum_{k=1}^{j-1}f_{i-1,k},\text{(if op=<)} fi,j=k=1j1fi1,k,(if op=<)
  • f i , j = ∑ k = j i − 1 f i − 1 , k , (if op=>) f_{i,j}=\sum_{k=j}^{i-1}f_{i-1,k},\text{(if op=>)} fi,j=k=ji1fi1,k,(if op=>)

你可能会疑惑第二个式子,因为对于 > > >,我们可以将 [ 1 , i − 1 ] [1,i-1] [1,i1] > j >j >j 的数都加上 1 1 1,这样能保证值域为 [ 1 , i ] [1,i] [1,i],且大小关系不变,并且可以从 i − 1 i-1 i1 转移。

转移可以用前缀和优化,时间复杂度 O ( n 2 ) O(n^2) O(n2)

#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
	return x*f;
}
const int N=3010,mod=1e9+7;
int n,f[N][N],s[N][N];char ch[N];
int main(){
	n=rd,scanf("%s",ch+1);
	f[1][1]=s[1][1]=1;
	FOR(i,2,n) FOR(j,1,i){
		if(ch[i-1]=='<') f[i][j]=s[i-1][j-1];
		else f[i][j]=(1ll*s[i-1][i-1]-s[i-1][j-1]+mod)%mod;
		s[i][j]=(1ll*s[i][j-1]+f[i][j])%mod;
	}
	printf("%d\n",s[n][n]);
	return 0;
}

U题

f S f_S fS 表示当前所选物品集合为 S S S 时所得的最大价值,转移为 f S = max ⁡ {   f S ′ + w S ⊕ S ′ } f_S=\max \{\ f_{S'}+w_{S\oplus S'}\} fS=max{ fS+wSS} ⊕ \oplus 为异或),枚举 S ′ ⊆ S S'\subseteq S SS 即可完成转移。

w S w_S wS 为将集合 S S S 中的数放入同一组中的价值,这个可以 O ( 2 n n 2 ) O(2^nn^2) O(2nn2) 预处理,然后转移 f f f 时枚举子集,最终复杂度为 O ( 2 n n 2 + 3 n ) O(2^nn^2+3^n) O(2nn2+3n)

#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
	return x*f;
}
const int N=16,M=(1<<N)+10;
int n,a[N][N];ll f[M],w[M];
int main(){
	n=rd;
	FOR(i,0,n-1) FOR(j,0,n-1) a[i][j]=rd;
	FOR(i,0,(1<<n)-1){
		ll res=0;
		FOR(j,0,n-1) FOR(k,j+1,n-1){
			if(((i>>j)&1)&&((i>>k)&1)) res+=a[j][k];
		}
		f[i]=w[i]=res;
	}
	FOR(i,0,(1<<n)-1){
		for(int s=i;s;s=(s-1)&i) f[i]=max(f[i],f[i^s]+w[s]);
	}
	printf("%lld\n",f[(1<<n)-1]);
	return 0;
}

V题

树形 dp \text{dp} dp 好题。

要求所有节点的答案,考虑换根 dp \text{dp} dp,先想根节点确定的做法。

f x f_x fx 表示以 x x x 为根的子树中, x x x 染成黑色所得到的连通块的方案数。

y y y x x x 的儿子,转移为 f x ← f x × ( f y + 1 ) f_x\gets f_x\times (f_y+1) fxfx×(fy+1),表示 y y y 可以染黑色或白色。

再考虑换根 dp \text{dp} dp,设 g x g_x gx 表示以 x x x 为根节点的子树外,染成黑色的节点与 x x x 构成一个连通块的方案数,考虑 f a fa fa 与它的兄弟的贡献: g x = g f a ∏ v ( f v + 1 ) + 1 , v ∈ brother(x) g_x=g_{fa}\prod_{v}(f_v+1)+1,v\in \text{brother(x)} gx=gfav(fv+1)+1,vbrother(x)

暴力求是 O ( n 2 ) O(n^2) O(n2),可以在第一遍 dfs \text{dfs} dfs 时求出 f f f 的前缀积、后缀积,就可以做到 O ( n ) O(n) O(n) 了。

#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
#define debug cout<<"I love CCF"<<endl;
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
	return x*f;
}
const int N=1e5+10;
int n,mod,head[N],tot,f[N],g[N],pre[N],nxt[N];
struct node{int to,nxt;}edge[N<<1];
void add(int x,int y){edge[++tot]={y,head[x]},head[x]=tot;}
void dfs(int x,int fa){
	f[x]=1;vector<int> v;
	for(int i=head[x];i;i=edge[i].nxt){
		int y=edge[i].to;if(y==fa) continue;
		dfs(y,x);
		f[x]=1ll*f[x]*(f[y]+1)%mod;
		v.push_back(y);
	}
	int res=1;
	if(!v.size()) return;
	for(int i=0;i<=v.size()-1;i++){
		pre[v[i]]=res,res=1ll*res*(f[v[i]]+1)%mod;
	}
	res=1;
	for(int i=v.size()-1;i>=0;i--){
		nxt[v[i]]=res,res=1ll*res*(f[v[i]]+1)%mod;
	}
}
void DFS(int x,int fa){
	if(fa==0) g[x]=1;
	else g[x]=(1ll*g[fa]*pre[x]%mod*nxt[x]%mod+1)%mod;
	for(int i=head[x];i;i=edge[i].nxt){
		int y=edge[i].to;if(y==fa) continue;
		DFS(y,x);
	}
}
int main(){
	n=rd,mod=rd;
	FOR(i,1,n-1){int x=rd,y=rd;add(x,y),add(y,x);}
	dfs(1,0),DFS(1,0);
	FOR(i,1,n){
		int ans=1ll*f[i]*g[i]%mod;
		printf("%d\n",ans);
	}
	return 0;
}

W题

区间贡献,直接做是很难转移的,所以把区间的右端点记下来,我们只在到达右端点时才更新这段区间,这样可以保证不重不漏。

f i , j f_{i,j} fi,j 表示前 i i i 个位置,上一个 1 1 1 j j j 位置得到的最大价值,显然转移有:

f i , i = max ⁡ j = 1 i − 1 f i − 1 , j + ∑ l k ≤ i ∧ r k = i a k \begin{array}{c} &&&&&&&&&&f_{i,i}=\max_{j=1}^{i-1}f_{i-1,j}+\sum_{l_k\le i\wedge r_k=i}a_k \end{array} fi,i=maxj=1i1fi1,j+lkirk=iak

f i , j = f i − 1 , j + ∑ l k ≤ j ∧ r k = i a k \begin{array}{c} &&&&&&&&&&f_{i,j}&=f_{i-1,j}+\sum_{l_k\le j\wedge r_k=i}a_k\\ \end{array} fi,j=fi1,j+lkjrk=iak

于是我们就有了 O ( n 2 ) O(n^2) O(n2) 的做法,滚动数组优化一下,空间复杂度为 O ( n ) O(n) O(n)

思考优化,我们发现对于 i = j i=j i=j,实际就是在 [ 1 , i − 1 ] [1,i-1] [1,i1] 查询了 f f f 的最大值,然后对于所有 r = i r=i r=i 的区间,它们都会对 [ l , r ] [l,r] [l,r] 内的 dp \text{dp} dp 值产生 a a a 的贡献。

所以我们用线段树维护 f f f,每到一个新位置执行两种操作:

  • 查询全局最大值,更新 i i i 位置。
  • 对于所有 r = i r=i r=i 的区间,在 [ l , r ] [l,r] [l,r] 区间内加上 a a a

区间加、区间最大值,线段树维护就好了,时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)

#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define PII pair<int,int>
#define fi first
#define se second
#define pb push_back
#define mp make_pair
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
	return x*f;
}
const int N=2e5+10;
int n,m;ll mx[N<<2],tag[N<<2];
vector<PII> d[N];
void pushdown(int u){
	tag[u<<1]+=tag[u],tag[u<<1|1]+=tag[u];
	mx[u<<1]+=tag[u],mx[u<<1|1]+=tag[u];
	tag[u]=0;
}
void pushup(int u){mx[u]=max(mx[u<<1],mx[u<<1|1]);}
void modify(int u,int l,int r,int ql,int qr,ll v){
	if(ql<=l&&r<=qr){
		mx[u]+=v,tag[u]+=v;
		return;
	}
	pushdown(u);
	int mid=(l+r)>>1;
	if(ql<=mid) modify(u<<1,l,mid,ql,qr,v);
	if(qr>mid) modify(u<<1|1,mid+1,r,ql,qr,v);
	pushup(u);
}
int main(){
	n=rd,m=rd;
	FOR(i,1,m){int l=rd,r=rd,v=rd;d[r].pb(mp(l,v));}
	FOR(i,1,n){
		modify(1,1,n,i,i,max(0ll,mx[1]));
		for(auto j:d[i]) modify(1,1,n,j.fi,i,(ll)j.se);
	}
	printf("%lld\n",max(mx[1],0ll));
	return 0;
}

X题

贪心 + + + dp \text{dp} dp 的好题。

有重量、体积、价值的关键字眼,考虑 01 01 01 背包,但发现暴力做是 O ( n 2 w ) O(n^2w) O(n2w) 的。

考虑贪心,对于两个物品 i , j i,j i,j,假如先放 i i i,后放 j j j,则 i i i 后面还能放 s i − w j s_i-w_j siwj 重量的物品,反之则为 s j − w i s_j-w_i sjwi 的物品。若先放 i i i,则应满足 s i − w j > s j − w i s_i-w_j>s_j-w_i siwj>sjwi,即 s i + w i > s j + w j s_i+w_i>s_j+w_j si+wi>sj+wj。所以按 s + w s+w s+w 从小到大排序,然后从上往下放,进行 01 01 01 背包的转移。

#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
	return x*f;
}
const int N=10010;
int n;ll f[N<<2];
struct node{int w,s,v;}a[N];
bool cmp(node a,node b){return a.w+a.s<b.w+b.s;}
int main(){
	n=rd;memset(f,-1,sizeof(f)),f[0]=0;
	FOR(i,1,n) a[i].w=rd,a[i].s=rd,a[i].v=rd;
	sort(a+1,a+1+n,cmp);
	FOR(i,1,n) ROF(j,a[i].s,0){//i上面放了j的物品,现在加上i
		f[j+a[i].w]=max(f[j+a[i].w],f[j]+a[i].v);
	}
	ll ans=0;
	FOR(i,0,20000) ans=max(ans,f[i]);
	printf("%lld\n",ans);
	return 0;
}

Y题

经典计数题。

正难则反,考虑容斥思想。首先不考虑限制,从 ( 1 , 1 ) → ( n , m ) (1,1)\to (n,m) (1,1)(n,m) 方案数为 ( n + m − 2 n − 1 ) \begin{pmatrix}n+m-2\\n-1\end{pmatrix} (n+m2n1),然后减去经过限制点的方案数。

如何减去经过限制点的方案数呢?如果用朴素的容斥是 2 2 2 的次方级别的,复杂度无法接受,并且太麻烦了。

所以考虑 dp \text{dp} dp,设 f i f_i fi 表示从 ( 1 , 1 ) (1,1) (1,1) 到第 i i i 个限制点,中间不经过其他限制点的方案数。

我们先按坐标排序,然后进行转移: f i = ( x i + y i − 2 x i − 1 ) − ∑ j = 1 i − 1 f j × ( x i − x j + y i − y j x i − x j ) f_i=\begin{pmatrix}x_i+y_i-2\\x_i-1\end{pmatrix}-\sum_{j=1}^{i-1}f_j\times \begin{pmatrix}x_i-x_j+y_i-y_j\\x_i-x_j\end{pmatrix} fi=(xi+yi2xi1)j=1i1fj×(xixj+yiyjxixj)

我们发现这样可以不重不漏的统计从 ( 1 , 1 ) → ( x i , y i ) (1,1)\to (x_i,y_i) (1,1)(xi,yi) 的方案数。

#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
	return x*f;
}
const int N=3010,M=2e5+10,mod=1e9+7;
int n,m,r,fac[M],infac[M],f[N];
struct node{int x,y;}a[N];
bool cmp(node a,node b){
	if(a.x==b.x) return a.y<b.y;
	return a.x<b.x;
}
int qpow(int a,int b){
	int res=1;
	while(b){if(b&1)res=1ll*res*a%mod;a=1ll*a*a%mod,b>>=1;}
	return res;
}
void init(){
	int t=2e5;fac[0]=1;
	FOR(i,1,t) fac[i]=1ll*fac[i-1]*i%mod;
	infac[t]=qpow(fac[t],mod-2);
	ROF(i,t-1,0) infac[i]=1ll*infac[i+1]*(i+1)%mod;
}
int C(int n,int m){
	if(n<0||m<0||n<m) return 0;
	return 1ll*fac[n]*infac[m]%mod*infac[n-m]%mod;
}
int main(){
	init();
	n=rd,m=rd,r=rd;FOR(i,1,r) a[i].x=rd,a[i].y=rd;
	r++,a[r]={n,m};
	sort(a+1,a+1+r,cmp);
	FOR(i,1,r){
		f[i]=C(a[i].x+a[i].y-2,a[i].x-1);
		FOR(j,1,i-1){
			f[i]=(1ll*f[i]-1ll*f[j]*C(a[i].x-a[j].x+a[i].y-a[j].y,a[i].x-a[j].x)%mod+mod)%mod;
		}
	}
	printf("%d\n",f[r]);
	return 0;
}

Z题

终于到最后一道题了,实际上就是一道斜率优化板子题。

朴素转移方程很好写: f i = min ⁡ j = 1 i − 1 { f j + ( h i − h j ) 2 + C } f_i=\min_{j=1}^{i-1}\{f_j+(h_i-h_j)^2+C\} fi=minj=1i1{fj+(hihj)2+C},暴力做是 O ( n 2 ) O(n^2) O(n2) 的。

min ⁡ \min min 里面的式子拆开:

f j + ( h i − h j ) 2 + C = f j + h i 2 + h j 2 − 2 h i h j + C = ( f j + h j 2 ) − 2 h j h i + C + h i 2 \begin{aligned} f_j+(h_i-h_j)^2+C&=f_j+h_i^2+h_j^2-2h_ih_j+C \\&=(f_j+h_j^2)-2h_jh_i+C+h_i^2 \end{aligned} fj+(hihj)2+C=fj+hi2+hj22hihj+C=(fj+hj2)2hjhi+C+hi2

所以令 F j = − 2 h j x + h j 2 + f j F_j=-2h_jx+h_j^2+f_j Fj=2hjx+hj2+fj,直接李超线段树,斜率优化啥的就滚远吧!

对于 i i i,我们就找到 x = h i x=h_i x=hi 时纵坐标最小的点,然后进行更新即可。

时间复杂度 O ( n l o g 2 n ) O(nlog^2n) O(nlog2n)

如果你不会李超线段树,或者想追求更优的时间复杂度的话,由于本题 h h h 单调递增的特殊性质,还有 O ( n ) O(n) O(n) 的斜率优化做法。

#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define PDI pair<double,int>
#define mp make_pair
#define int long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
	return x*f;
}
const int N=1e6+10,M=1e6;
const double eps=1e-10;
int n,h[N],seg[N<<2],C,f[N];
struct node{double k,b;}p[N];
int cmp(double x,double y){
	if(x-y>eps) return -1;
	if(y-x>eps) return 1;
	return 0;
}
double cal(int id,int x){
	return p[id].k*x+p[id].b;
}
void update(int u,int l,int r,int id){
	int &g=seg[u],mid=(l+r)>>1;
	if(cmp(cal(id,mid),cal(g,mid))==1) swap(g,id);
	if(cmp(cal(id,l),cal(g,l))==1) update(u<<1,l,mid,id);
	if(cmp(cal(id,r),cal(g,r))==1) update(u<<1|1,mid+1,r,id);
}
void modify(int u,int l,int r,int ql,int qr,int id){
	if(ql<=l&&r<=qr){update(u,l,r,id);return;}
	int mid=(l+r)>>1;
	if(ql<=mid) modify(u<<1,l,mid,ql,qr,id);
	if(qr>mid) modify(u<<1|1,mid+1,r,ql,qr,id);
}
PDI pmx(PDI a,PDI b){
	if(cmp(a.first,b.first)>0) return a;
	else return b;
}
PDI query(int u,int l,int r,int v){
	if(l>v||r<v) return mp(0,0);
	int mid=(l+r)>>1;PDI res=mp(cal(seg[u],v),seg[u]);
	if(l==r) return res;
	return pmx(res,pmx(query(u<<1,l,mid,v),query(u<<1|1,mid+1,r,v)));
}
signed main(){
	n=rd,C=rd;
	FOR(i,1,n) h[i]=rd;
	p[0]={0,1e16};
	p[1]={-2.0*h[1],1.0*h[1]*h[1]};
	modify(1,1,M,1,M,1);
	FOR(i,2,n){
		int t=query(1,1,M,h[i]).second;
		f[i]=p[t].k*h[i]+p[t].b+C+h[i]*h[i];
		p[i]={-2.0*h[i],1.0*h[i]*h[i]+f[i]};
		modify(1,1,M,1,M,i);
	}
	printf("%lld\n",f[n]);
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值