【做题记录】dp(马思博)

A. 【模板】动态 DP

好像是我做过的最难的一道 DDP(?)

普通的转移 \(\begin{cases}f_{u,0}=\sum\max(f_{v,0},f_{v,1})\\f_{u,1}=a_u+\sum f_{v,0}\end{cases}\) 是不好优化的,我们希望 \(f_{u,0/1}\) 只从一个节点转移来。不妨设其中一个节点为 \(v\)(实际上就是重儿子,至于为什么一会儿再说),设 \(g_{u,0}\) 表示不选 \(u\),只考虑轻儿子的最大独立集,\(g_{u,1}\) 表示选 \(u\),只考虑轻儿子的最大独立集。于是有新转移 \(\begin{cases}f_{u,0}=g_{u,0}+\max(f_{v,0},f_{v,1})\\f_{u,1}=g_{u,1}+f_{v,0}\end{cases}\)。于是可以用线段树维护矩阵,由于从下往上转移,矩阵设计为左乘的形式比较方便。每次修改将 \(u\) 的根链所有链头的父亲的矩阵改变即可。这也就是重剖的原因,保证了时间复杂度线性对数方。

Code
#include<bits/stdc++.h>
#define int long long
#define il inline
#define pb push_back
#define lid id<<1
#define rid id<<1|1
using namespace std;
namespace asbt{
const int maxn=1e5+5,inf=1e18;
int n,m,a[maxn],sz[maxn],hes[maxn],fa[maxn];
int dfn[maxn],cnt,stk[maxn],top[maxn];
int bot[maxn],f[maxn][2];
vector<int> e[maxn];
struct juz{
	int a[2][2];
	juz(){
		a[0][0]=a[0][1]=a[1][0]=a[1][1]=-inf;
	}
	il int*operator[](int x){
		return a[x];
	}
	il juz operator*(juz x)const{
		juz r;
		for(int i:{0,1}){
			for(int j:{0,1}){
				r[i][j]=max(a[i][0]+x[0][j],a[i][1]+x[1][j]);
			}
		}
		return r;
	}
}b[maxn],tr[maxn<<2];
il void dfs1(int u){
	sz[u]=1;
	int mxs=0;
	for(int v:e[u]){
		if(v==fa[u]){
			continue;
		}
		fa[v]=u;
		dfs1(v);
		sz[u]+=sz[v];
		if(mxs<sz[v]){
			mxs=sz[v],hes[u]=v;
		}
	}
}
il void dfs2(int u){
	if(!top[u]){
		top[u]=u;
	}
	dfn[u]=++cnt,stk[cnt]=u;
	bot[top[u]]=u;
	if(hes[u]){
		top[hes[u]]=top[u];
		dfs2(hes[u]);
	}
	int &g0=b[u][0][0]=0,&g1=b[u][1][0]=a[u];
	for(int v:e[u]){
		if(v==fa[u]||v==hes[u]){
			continue;
		}
		dfs2(v);
		g0+=max(f[v][0],f[v][1]);
		g1+=f[v][0];
	}
	b[u][0][1]=g0;
	f[u][0]=0,f[u][1]=a[u];
	for(int v:e[u]){
		if(v==fa[u]){
			continue;
		}
		f[u][0]+=max(f[v][0],f[v][1]);
		f[u][1]+=f[v][0];
	}
}
il void pushup(int id){
	tr[id]=tr[lid]*tr[rid];
}
il void build(int id,int l,int r){
	if(l==r){
		tr[id]=b[stk[l]];
		return ;
	}
	int mid=(l+r)>>1;
	build(lid,l,mid);
	build(rid,mid+1,r);
	pushup(id);
}
il void upd(int id,int l,int r,int p){
	if(l==r){
		tr[id]=b[stk[p]];
		return ;
	}
	int mid=(l+r)>>1;
	if(p<=mid){
		upd(lid,l,mid,p);
	}else{
		upd(rid,mid+1,r,p);
	}
	pushup(id);
}
il juz query(int id,int L,int R,int l,int r){
	if(L>=l&&R<=r){
		return tr[id];
	}
	int mid=(L+R)>>1;
	if(r<=mid){
		return query(lid,L,mid,l,r);
	}
	if(l>mid){
		return query(rid,mid+1,R,l,r);
	}
	return query(lid,L,mid,l,r)*query(rid,mid+1,R,l,r);
}
int main(){
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	for(int i=1,u,v;i<n;i++){
		cin>>u>>v;
		e[u].pb(v),e[v].pb(u);
	}
	dfs1(1),dfs2(1),build(1,1,n);
//	for(int i=1;i<=n;i++){
//		cout<<i<<' '<<sz[i]<<' '<<hes[i]<<' '<<dfn[i]<<' '<<top[i]<<' '<<bot[i]<<'\n';
//	}
	while(m--){
		int u,x;
		cin>>u>>x;
		b[u][1][0]+=x-a[u],a[u]=x;
		while(u){
			juz p=query(1,1,n,dfn[top[u]],dfn[bot[top[u]]]);
			upd(1,1,n,dfn[u]);
			juz q=query(1,1,n,dfn[top[u]],dfn[bot[top[u]]]);
			u=fa[top[u]];
			int &g0=b[u][0][0],&g1=b[u][1][0];
			g0-=max(p[0][0],p[1][0]);
			g0+=max(q[0][0],q[1][0]);
			g1-=p[0][0],g1+=q[0][0];
			b[u][0][1]=g0;
		}
		juz r=query(1,1,n,dfn[1],dfn[bot[1]]);
		cout<<max(r[0][0],r[1][0])<<'\n';
	}
	return 0;
}
}
signed main(){return asbt::main();}

C. [POI 2011] Lightning Conductor

相当于是对于每个 \(i\),求 \(\max\{a_j+\sqrt{i-j}-a_i\}\)。满足决策单调性,分治即可。

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
using namespace std;
namespace asbt{
const int maxn=5e5+5,inf=1e9;
int n,a[maxn],ans[maxn];
double sq[maxn];
il void solve(int L,int R,int l,int r){
	if(l>r){
		return ;
	}
	int mid=(l+r)>>1,p=0;
	double v=-inf;
	for(int i=L;i<=min(mid,R);i++){
		double t=a[i]-a[mid]+sq[mid-i];
		if(v<t){
			p=i,v=t;
		}
	}
	ans[mid]=max(ans[mid],(int)ceil(v));
	solve(L,p,l,mid-1);
	solve(p,R,mid+1,r);
}
int main(){
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		sq[i]=sqrt(i);
	}
	memset(ans,-0x3f,sizeof(ans));
	solve(1,n,1,n);
	reverse(a+1,a+n+1);
	reverse(ans+1,ans+n+1);
	solve(1,n,1,n);
	for(int i=n;i;i--){
		cout<<max(ans[i],0)<<'\n';
	}
	return 0;
}
}
int main(){return asbt::main();}

D. [国家集训队] Tree I

我们设选了 \(i\) 条白边时的最小生成树为 \(f_i\),于是得到了若干个点 \((i,f_i)\)。打表可得,这些点组成了一个下凸包。我们考虑二分斜率 \(k\),找到切点 \((i,f_i)\),当 \(i=need\)\(f_i\) 就是答案。如下图:

1

问题转变为求切点。容易发现,在所有斜率为 \(k\) 的直线中,切点所在的那一条纵截距最小。如下图:

2

那么我们怎么求最小的纵截距呢?设这条直线为 \(y=kx+b\),则有 \(kx+b=f_x\),于是纵截距即为 \(b=f_x-kx\),也就是给每条白边都减去了 \(k\)。于是我们给白边减去 \(k\) 再求最小生成树即可。时间复杂度线性对数。

实际上,这个算法叫做 wqs 二分。解决这种“钦定选 \(need\) 个物品时的最值”问题时经常使用 wqs 二分,它的特点是要求 \(f_i\) 必须是凸函数、细节多得像屎。至于本题中的二分上下界,因为凸包的最小/大斜率一定不超过 \([-100,100]\),因此定为 \([-100,100]\) 即可。

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
using namespace std;
namespace asbt{
const int maxn=1e5+5;
int n,m,kk,ca,cb,fa[maxn];
struct edge{
	int u,v,w,opt;
	il bool operator<(const edge &x)const{
		return w<x.w;
	}
}a[maxn],b[maxn],c[maxn];
il int find(int x){
	return x!=fa[x]?fa[x]=find(fa[x]):x;
}
il bool check(int x){
	int p1=1,p2=1,p3=1;
	while(p1<=ca&&p2<=cb){
		if(a[p1].w-x<=b[p2].w){
			c[p3++]=a[p1++];
		}
		else{
			c[p3++]=b[p2++];
		}
	}
	while(p1<=ca){
		c[p3++]=a[p1++];
	}
	while(p2<=cb){
		c[p3++]=b[p2++];
	}
	for(int i=0;i<n;i++){
		fa[i]=i;
	}
	int cnt=0;
	for(int i=1;i<=m;i++){
		int u=find(c[i].u),v=find(c[i].v);
		if(u!=v){
			fa[u]=v,cnt+=c[i].opt^1;
		}
	}
	return cnt>=kk;
}
int main(){
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>m>>kk;
	for(int i=1,u,v,w,opt;i<=m;i++){
		cin>>u>>v>>w>>opt;
		if(!opt){
			a[++ca]={u,v,w,opt};
		}
		else{
			b[++cb]={u,v,w,opt};
		}
	}
	sort(a+1,a+ca+1),sort(b+1,b+cb+1);
	int l=-100,r=100;
	while(l<r){
		int mid=(l+r)>>1;
		if(check(mid)){
			r=mid;
		}
		else{
			l=mid+1;
		}
	}
	int p1=1,p2=1,p3=1;
	while(p1<=ca&&p2<=cb){
		if(a[p1].w-l<=b[p2].w){
			c[p3]=a[p1++];
			c[p3++].w-=l;
		}
		else{
			c[p3++]=b[p2++];
		}
	}
	while(p1<=ca){
		c[p3]=a[p1++];
		c[p3++].w-=l;
	}
	while(p2<=cb){
		c[p3++]=b[p2++];
	}
	for(int i=0;i<n;i++){
		fa[i]=i;
	}
	int ans=0;
	for(int i=1;i<=m;i++){
		int u=find(c[i].u),v=find(c[i].v);
		if(u!=v){
			fa[u]=v,ans+=c[i].w;
		}
	}
	cout<<ans+kk*l;
	return 0;
}
}
int main(){return asbt::main();}

E. Gosha is hunting

显然有三维的 DP:设 \(f_{i,j,k}\) 表示考虑到第 \(i\) 个神奇宝贝,用了 \(j\) 个宝贝球和 \(k\) 个超级球的最大期望。可以发现 \(f_{i,j,k}\) 是关于 \(k\) 的上凸包,wqs 二分即可。时间复杂度 \(O(n^2\log V)\)

LG 上的这篇讨论中有相当多的 hack 数据,非常毒瘤的是那组号称更小但更强的数据是错的(害得我干瞪着代码看了十五分钟。。。

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
using namespace std;
namespace asbt{
const int maxn=2e3+5;
const double eps=1e-8;
int n,a,b,g[maxn][maxn];
double p[maxn],q[maxn];
double f[maxn][maxn];
il bool check(double x){
	for(int i=1;i<=n;i++){
		for(int j=0;j<=a;j++){
			f[i][j]=g[i][j]=0;
			if(f[i][j]<f[i-1][j]){
				f[i][j]=f[i-1][j];
				g[i][j]=g[i-1][j];
			}
			if(j&&f[i][j]<f[i-1][j-1]+p[i]){
				f[i][j]=f[i-1][j-1]+p[i];
				g[i][j]=g[i-1][j-1];
			}
			if(f[i][j]<f[i-1][j]+q[i]-x){
				f[i][j]=f[i-1][j]+q[i]-x;
				g[i][j]=g[i-1][j]+1;
			}
			if(j&&f[i][j]<f[i-1][j-1]+p[i]+q[i]-x-p[i]*q[i]){
				f[i][j]=f[i-1][j-1]+p[i]+q[i]-x-p[i]*q[i];
				g[i][j]=g[i-1][j-1]+1;
			}
		}
	}
	return g[n][a]<=b;
}
int main(){
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>a>>b;
	for(int i=1;i<=n;i++){
		cin>>p[i];
	}
	for(int i=1;i<=n;i++){
		cin>>q[i];
	}
	double l=0,r=1;
	while(r-l>eps){
		double mid=(l+r)/2;
		if(check(mid)){
			r=mid;
		}else{
			l=mid;
		}
	}
	check(l);
//	cout<<l<<' '<<g[n][a]<<'\n';
	cout<<fixed<<setprecision(6)<<f[n][a]+l*b;
	return 0;
}
}
int main(){return asbt::main();}

H. 小N的独立集

最大独立集有一个经典 DP:设 \(dp_{i,0/1}\) 表示选/不选 \(i\)\(i\) 的子树的最大独立集。因为要求满足 \(dp_{i,0/1}=x\) 的树的数量, 所以考虑 DP 套 DP:设 \(f_{i,j,k}\) 表示 \(dp_{i,0}=j,dp_{i,1}=k\) 的方案数。但这样时间空间都是开不下的,考虑修改 DP 数组的定义,\(dp_{i,0/1}\) 表示强制/不强制不选 \(i\) 的最大独立集,\(f_{i,j,k}\) 表示 \(dp_{i,0}=j,dp_{i,1}=j+k\) 的方案数,因为 \(k\le5\) 所以直接树上背包转移即可。

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
#define pb push_back
using namespace std;
namespace asbt{
const int maxn=1e3+5,mod=1e9+7;
int n,m,sz[maxn];
ll f[maxn][maxn*5][7],g[maxn*5][7];
vector<int> e[maxn];
il void dfs(int u,int fa){
	sz[u]=1;
	for(int i=1;i<=m;i++){
		f[u][0][i]=1;
	}
	for(int v:e[u]){
		if(v==fa){
			continue;
		}
		dfs(v,u);
		for(int i=0;i<=(sz[u]+sz[v])*m;i++){
			for(int j=0;j<=m;j++){
				g[i][j]=0;
			}
		}
		for(int i1=0;i1<=sz[u]*m;i1++){
			for(int j1=0;j1<=m;j1++){
				if(!f[u][i1][j1]){
					continue;
				}
				for(int i2=0;i2<=sz[v]*m;i2++){
					for(int j2=0;j2<=m;j2++){
						if(!f[v][i2][j2]){
							continue;
						}
						(g[i1+i2+j2][max(j1-j2,0)]+=f[u][i1][j1]*f[v][i2][j2])%=mod;
					}
				}
			}
		}
		sz[u]+=sz[v];
		for(int i=0;i<=sz[u]*m;i++){
			for(int j=0;j<=m;j++){
				f[u][i][j]=g[i][j];
			}
		}
	}
}
int main(){
	freopen("nset.out","w",stdout);
	freopen("nset.in","r",stdin);
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>m;
	for(int i=1,u,v;i<n;i++){
		cin>>u>>v;
		e[u].pb(v),e[v].pb(u);
	}
	dfs(1,0);
	for(int i=1;i<=n*m;i++){
		ll ans=0;
		for(int j=0;j<=min(i,m);j++){
			ans+=f[1][i-j][j];
		}
		cout<<ans%mod<<"\n";
	}
	return 0;
}
}
int main(){return asbt::main();}

I. [NOI Online #1 入门组] 跑步

问题等价于划分成若干无序的数,\(O(n^2)\) 的多重背包是显然的。

考虑根号分治,对于划分中 \(<\sqrt{n}\) 的数,用多重背包求;对于 \(\ge\sqrt{n}\) 的数,设有 \(i\) 个,和为 \(j\) 的方案数为 \(g_{j,i}\),于是有 \(g_{j,i}=g_{j-i,i}+g_{j-m,i-1}\),相当于是加入一个 \(m\) 或给每个数加 \(1\)。最后统计答案即可,时间复杂度 \(O(n\sqrt{n})\)

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
using namespace std;
namespace asbt{
const int maxn=1e5+5;
int n,m,mod,f[maxn],g[maxn][325];
il int pls(int x,int y){
	return x+y<mod?x+y:x+y-mod;
}
il int mns(int x,int y){
	return x<y?x-y+mod:x-y;
}
il void add(int &x,int y){
	x=pls(x,y);
}
il void sub(int &x,int y){
	x=mns(x,y);
}
int main(){
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>mod;
	m=sqrt(n)+1;
	f[0]=1;
	for(int i=1;i<m;i++){
		for(int j=i;j<=n;j++){
			add(f[j],f[j-i]);
		}
	}
	g[0][0]=1;
	for(int i=1;i<m;i++){
		for(int j=i;j<=n;j++){
			g[j][i]=g[j-i][i];
			if(j>=m){
				add(g[j][i],g[j-m][i-1]);
			}
		}
	}
	int ans=0;
	for(int i=0;i<=n;i++){
		int sum=0;
		for(int j=0;j<m;j++){
			add(sum,g[i][j]);
		}
		ans=(f[n-i]*1ll*sum+ans)%mod;
	}
	cout<<ans;
	return 0;
}
}
int main(){return asbt::main();}

J. [agc001_e]BBQ Hard

\(a_i+b_i+a_j+b_j\choose a_i+b_i\) 其实就等价于从 \((-a_i,-b_i)\) 走到 \((a_j,b_j)\) 的方案数,于是 \(O(n^2)\) DP 即可。需要减去 \(i\) 自己给自己的贡献,还有 \(i>j\) 的贡献。

Code
#include<bits/stdc++.h>
#define int long long
#define il inline
using namespace std;
namespace asbt{
const int maxn=2e5+5,maxm=4e3+5,mod=1e9+7,inv2=5e8+4;
int n,a[maxn],b[maxn],fac[maxm<<1],inv[maxm<<1],f[maxm][maxm];
il int qpow(int x,int y=mod-2){
	int res=1;
	while(y){
		if(y&1){
			(res*=x)%=mod;
		}
		(x*=x)%=mod,y>>=1;
	}
	return res;
}
il void init(int n=8e3){
	fac[0]=1;
	for(int i=1;i<=n;i++){
		fac[i]=fac[i-1]*i%mod;
	}
	inv[n]=qpow(fac[n]);
	for(int i=n;i;i--){
		inv[i-1]=inv[i]*i%mod;
	}
}
il int C(int x,int y){
	if(x<y||y<0){
		return 0;
	}
	return fac[x]*inv[y]%mod*inv[x-y]%mod;
}
int main(){
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n;
	init();
	int ans=0;
	for(int i=1;i<=n;i++){
		cin>>a[i]>>b[i];
		(ans+=mod-C((a[i]+b[i])<<1,a[i]<<1))%=mod;
//		cout<<C((a[i]+b[i])<<1,a[i]<<1)<<"\n";
		f[2001-a[i]][2001-b[i]]++;
	}
	for(int i=1;i<=4001;i++){
		for(int j=1;j<=4001;j++){
			f[i][j]=(f[i][j]+f[i-1][j]+f[i][j-1])%mod;
		}
	}
	for(int i=1;i<=n;i++){
		(ans+=f[2001+a[i]][2001+b[i]])%=mod;
//		cout<<f[2001+a[i]][2001+b[i]]<<"\n";
	}
	cout<<ans*inv2%mod;
	return 0;
}
}
signed main(){return asbt::main();}

M. [CEOI 2016] kangaroo

问题可以转化为求 \(p_1=s\)\(p_n=t\)\(p_i\) 两边同时大于/小于 \(p_i\) 的排列 \(p\) 的个数。

考虑连续段 DP,设 \(f_{i,j}\) 表示考虑前 \(i\) 个数,分成了 \(j\) 段的方案数。转移分两种:

  • 新开一段,\(f_{i,j}\leftarrow(j-[i>s]-[i>t])\times f_{i-1,j-1}\)

  • 将两段连起来,\(f_{i,j}\leftarrow j\times f_{i-1,j+1}\)

注意特判 \(i=s\)\(t\) 的情况。时间复杂度 \(O(n^2)\)

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
using namespace std;
namespace asbt{
const int maxn=2e3+5,mod=1e9+7;
int n,s,t,f[maxn][maxn];
int main(){
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>s>>t;
	f[1][1]=1;
	for(int i=2;i<=n;i++){
		for(int j=1;j<=i;j++){
			if(i==s||i==t){
				f[i][j]=(f[i-1][j-1]+f[i-1][j])%mod;
			}
			else{
				f[i][j]=(f[i-1][j-1]*1ll*(j-(i>s)-(i>t))+f[i-1][j+1]*1ll*j)%mod;
			}
		}
	}
	cout<<f[n][1];
	return 0;
}
}
int main(){return asbt::main();}

N. [CSP-S 2022] 数据传输

\(K\) 分讨。

\(K=1\),直接倍增求和即可。

\(K=2\),考虑将路径拉出来 DP。设 \(f_i\) 表示走到第 \(i\) 个点的最小花费。故有 \(f_i=\min(f_{i-1},f_{i-2})+a_i\)。考虑 DDP,可以构造出转移矩阵:

\[\begin{bmatrix} f_i&f_{i-1} \end{bmatrix} =\begin{bmatrix} f_{i-1}&f_{i-2} \end{bmatrix} \times\begin{bmatrix} a_i&0\\ a_i&+\infty \end{bmatrix} \]

\(K=3\),此时我们发现可以走到路径上某个点的某个相邻点来减小花费。如下图:

image

可以发现每个点挂的点要么是相邻的最小值,要么就不挂点。记这个挂的点为 \(b_i\)。类似地仍然考虑 DP。设 \(f_{i,0/1}\) 表示走到 \(i\)/\(i\) 挂的点所需的最小花费。于是有转移:

\[\begin{cases} \begin{aligned} &f_{i,0}=\min(f_{i-1,0},f_{i-1,1},f_{i-2,0},f_{i-2,1},f_{i-3,0})+a_i\\ &f_{i,1}=\min(f_{i,0},f_{i-1,0},f_{i-1,1},f_{i-2,0})+b_i \end{aligned} \end{cases} \]

但是这个东西如果用矩阵优化,代码会带上 \(125\) 的常数。考虑换个定义。发现 DP 转移只和到 \(i\) 的距离有关,所以设 \(f_{i,0/1/2}\) 表示走到到 \(i\) 的距离为 \(0/1/2\) 的点的最小花费,于是有转移:

\[\begin{cases} \begin{aligned} &f_{i,0}=\min(f_{i-1,0},f_{i-1,1},f_{i-1,2})+a_i\\ &f_{i,1}=\min(\min(f_{i,0},f_{i-1,1})+b_i,f_{i-1,0})\\ &f_{i,2}=f_{i-1,1} \end{aligned} \end{cases} \]

进而可以设计出转移矩阵:

\[\begin{bmatrix} f_{i,0}&f_{i,1}&f_{i,2} \end{bmatrix} =\begin{bmatrix} f_{i-1,0}&f_{i-1,1}&f_{i-1,2} \end{bmatrix} \times\begin{bmatrix} a_i&0&+\infty\\ a_i&b_i&0\\ a_i&a_i+b_i&+\infty \end{bmatrix} \]

于是倍增即可,时间复杂度 \(O(n\log n)\)

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
#define pb push_back
using namespace std;
namespace asbt{
const int maxn=2e5+5;
const ll inf=1e18;
int n,m,kk,anc[maxn][18],dep[maxn];
ll a[maxn],b[maxn];
vector<int> e[maxn];
struct juz{
	ll mat[4][4];
	juz(bool x=false){
		for(int i=1;i<=kk;i++){
			for(int j=1;j<=kk;j++){
				mat[i][j]=inf;
			}
		}
		if(x){
			for(int i=1;i<=kk;i++){
				mat[i][i]=0;
			}
		}
	}
	juz(int x){
		switch(kk){
			case 1:{
				mat[1][1]=a[x];
				break;
			}
			case 2:{
				mat[1][1]=mat[2][1]=a[x];
				mat[1][2]=0,mat[2][2]=inf;
				break;
			}
			default:{
				mat[1][1]=mat[2][1]=mat[3][1]=a[x];
				mat[1][2]=mat[2][3]=0;
				mat[1][3]=mat[3][3]=inf;
				mat[2][2]=b[x],mat[3][2]=a[x]+b[x];
				break;
			}
		}
	}
	il ll*operator[](int x){
		return mat[x];
	}
	il juz operator*(juz x)const{
		juz res;
		for(int i=1;i<=kk;i++){
			for(int j=1;j<=kk;j++){
				for(int k=1;k<=kk;k++){
					res[i][j]=min(res[i][j],mat[i][k]+x[k][j]);
				}
			}
		}
		return res;
	}
}up[maxn][18],dw[maxn][18];
il void dfs1(int u,int fa){
	b[u]=inf;
	for(int v:e[u]){
		b[u]=min(b[u],a[v]);
		if(v==fa){
			continue;
		}
		dfs1(v,u);
	}
}
il void dfs2(int u,int fa){
	anc[u][0]=fa,dep[u]=dep[fa]+1;
	up[u][0]=dw[u][0]=juz(u);
	for(int i=1;i<=17;i++){
		anc[u][i]=anc[anc[u][i-1]][i-1];
		up[u][i]=up[u][i-1]*up[anc[u][i-1]][i-1];
		dw[u][i]=dw[anc[u][i-1]][i-1]*dw[u][i-1];
	}
	for(int v:e[u]){
		if(v==fa){
			continue;
		}
		dfs2(v,u);
	}
}
il juz dp(int u,int v){
	juz resu,resv(true);
	if(dep[u]==dep[v]){
		resv=dw[v][0],v=anc[v][0];
	}
	else if(dep[u]<dep[v]){
		swap(u,v);
	}
	resu[1][1]=a[u],u=anc[u][0];
	int ddep=dep[u]-dep[v],d=0;
//	if(!m){
//		cout<<"kpo "<<ddep<<"\n";
//	}
	while(ddep){
		if(ddep&1){
			resu=resu*up[u][d];
			u=anc[u][d];
		}
		ddep>>=1,d++;
	}
	if(u==v){
		return resu*up[u][0]*resv;
	}
	for(int i=17;~i;i--){
		if(anc[u][i]!=anc[v][i]){
			resu=resu*up[u][i];
			resv=dw[v][i]*resv;
			u=anc[u][i],v=anc[v][i];
		}
	}
	return resu*up[u][1]*dw[v][0]*resv;
}
int main(){
//	freopen("P8820_1.in","r",stdin);
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>m>>kk;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	for(int i=1,u,v;i<n;i++){
		cin>>u>>v;
		e[u].pb(v),e[v].pb(u);
	}
	dfs1(1,0),dfs2(1,0);
	while(m--){
		int u,v;
		cin>>u>>v;
		cout<<dp(u,v)[1][1]<<"\n";
	}
	return 0;
}
}
int main(){return asbt::main();}

O. [NOI2009] 诗人小G

\(dp_i\) 表示考虑到 \(i\) 的最小不协调度,则有方程:

\[dp_i=\min_{j=0}^{i-1}\{dp_j+|sum_i-sum_j+i-j-1-L|^P\} \]

后面那个东西满足四边形不等式,因此可以决策单调性优化。维护决策点队列,二分出每个决策点可以作为最优决策点的范围即可转移。时间复杂度线性对数。需要 long double

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
#define ld long double
using namespace std;
namespace asbt{
const int maxn=1e5+5;
const ld inf=1e18;
int T,n,m,kk,a[maxn],pre[maxn],q[maxn];
ld dp[maxn];
string s[maxn];
il ld qpow(ld x,int y){
	ld res=1;
	while(y){
		if(y&1){
			res*=x;
		}
		x*=x,y>>=1;
	}
	return res;
}
il ld calc(int x,int y){
	return dp[x]+qpow(abs(a[y]-a[x]+y-x-1-m),kk);
}
il int find(int x,int y){
	int l=y,r=n+1;
	while(l<r){
		int mid=(l+r)>>1;
		if(calc(x,mid)>=calc(y,mid)){
			r=mid;
		}
		else{
			l=mid+1;
		}
	}
	return l;
}
il void print(int x){
	if(!x){
		return ;
	}
	print(pre[x]);
	for(int i=pre[x]+1;i<x;i++){
		cout<<s[i]<<" ";
	}
	cout<<s[x]<<"\n";
}
il void solve(){
	cin>>n>>m>>kk;
	for(int i=1;i<=n;i++){
		cin>>s[i];
		a[i]=a[i-1]+s[i].size();
		dp[i]=inf,pre[i]=0;
	}
	int hd=1,tl=0;
	dp[0]=0,q[++tl]=0;
	for(int i=1;i<=n;i++){
		while(hd<tl&&find(q[hd],q[hd+1])<=i){
			hd++;
		}
		dp[i]=calc(q[hd],i),pre[i]=q[hd];
		while(hd<tl&&find(q[tl-1],q[tl])>=find(q[tl],i)){
			tl--;
		}
		q[++tl]=i;
	}
	if(dp[n]>inf){
		cout<<"Too hard to arrange\n";
	}
	else{
		cout<<(ll)dp[n]<<"\n";
		print(n);
	}
	cout<<"--------------------\n";
}
int main(){
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>T;
	while(T--){
		solve();
	}
	return 0;
}
}
int main(){return asbt::main();}

P. 忘情

\(dp_{i,j}\) 表示到 \(i\) 分了 \(j\) 段的最小值,于是有转移:

\[dp_{i,j}=\min_{k=0}^{i-1}\{dp_{k,j-1}+(sum_i-sum_k+1)^2\} \]

发现 \(dp_{i,j}\) 关于 \(j\) 是一个下凸函数,于是可以 wqs 二分。注意为了使截距更小,在截到 \(m\) 点时要继续让斜率变大。

于是内层 DP 是这样一个东西:设 \(f_i\) 为考虑到 \(i\) 的答案,有 \(f_i=\min\limits_{j=0}^{i-1}\{f_j+(sum_i-sum_j+1)^2-k\}\)。化简得 \(f_j+s_j^2-2s_j=2s_is_j+f_i-s_i^2-2s_i-1+k\)。于是有 \(k_i=2s_i\)\(b_i=f_i-s_i^2-2s_i-1+k\)\(x_j=s_j\)\(y_j=f_j+s_j^2-2s_j\)。因为是最小值,我们用单调队列维护 \(j\) 组成的下凸包,用斜率为 \(2s_i\) 的直线去切这个下凸包,斜率优化 DP 即可。

Code
#include<bits/stdc++.h>
#define int long long
#define il inline
using namespace std;
namespace asbt{
const int maxn=1e5+5;
int n,m,a[maxn],f[maxn],g[maxn],q[maxn];
il bool check(int x){
	int hd=1,tl=0;
	f[0]=0,q[++tl]=0;
	#define X(i) (a[i])
	#define Y(i) (f[i]+a[i]*a[i]-2*a[i])
	#define K(i,j) ((Y(j)-Y(i))*1.0l/(X(j)-X(i)))
	for(int i=1;i<=n;i++){
		while(hd<tl&&K(q[hd],q[hd+1])<2*a[i]){
			hd++;
		}
		f[i]=f[q[hd]]+(a[i]-a[q[hd]]+1)*(a[i]-a[q[hd]]+1)-x;
		g[i]=g[q[hd]]+1;
		while(hd<tl&&K(q[tl-1],q[tl])>K(q[tl],i)){
			tl--;
		}
		q[++tl]=i;
	}
	#undef X
	#undef Y
	#undef K
	return g[n]<=m;
}
int main(){
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		a[i]+=a[i-1];
	}
	int l=-2e16,r=0;
	while(l<r){
		int mid=(l+r+1)>>1;
		if(check(mid)){
			l=mid;
		}
		else{
			r=mid-1;
		}
	}
	check(l);
	cout<<f[n]+m*l;
	return 0;
}
}
signed main(){return asbt::main();}

Q. [ICPC 2018 WF] Gem Island

首先考虑每一种情况的概率。设最后每个人的宝石数为 \(c_i\),于是方案数为 \(\prod\limits_{i=1}^{n}{d-\sum_{j=1}^{i-1}(c_j-1)\choose(c_i-1)}\times\prod\limits_{i=1}^{n}(c_i-1)!=d!\)。于是每种情况的概率都是一样的,我们只需 DP 出方案数和总和即可。

先不考虑最开始那 \(n\) 个宝石。设 \(f_{i,j}\) 表示当前共有 \(i\) 个宝石,拥有最多宝石的人有 \(j\) 个时的方案数,于是有转移方程:

\[\forall k\in[1,j],f_{i+k,k}\leftarrow f_{i,j}\times{j\choose k} \]

类似地,设 \(g_{i,j}\) 表示此时前 \(r\) 大的总和,于是有方程:

\[\forall k\in[1,j],g_{i+k,k}\leftarrow(g_{i,j}+\min(k,r)\times f_{i,j})\times{j\choose k} \]

答案即为 \(\frac{\sum_{i=1}^{n}f_{d,i}}{\sum_{i=1}^{n}g_{d,i}}\)。时间复杂度 \(O(n^3)\)

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
using namespace std;
namespace asbt{
const int maxn=1e3+5;
int n,d,r;
double f[maxn][maxn],g[maxn][maxn],C[maxn][maxn];;
il void init(int n=1e3){
    C[0][0]=1;
    for(int i=1;i<=n;i++){
        C[i][0]=1;
        for(int j=1;j<=i;j++){
            C[i][j]=C[i-1][j-1]+C[i-1][j];
        }
    }
}
int main(){
    ios::sync_with_stdio(0),cin.tie(0);
    cin>>n>>d>>r;
    init();
    f[0][n]=1;
    for(int i=0;i<=d;i++){
        for(int j=1;j<=n;j++){
            for(int k=1;k<=j;k++){
                f[i+k][k]+=f[i][j]*C[j][k];
                g[i+k][k]+=(g[i][j]+min(k,r)*f[i][j])*C[j][k];
            }
        }
    }
    double sg=0,sf=0;
    for(int i=1;i<=n;i++){
        sg+=g[d][i],sf+=f[d][i];
    }
    cout<<fixed<<setprecision(7)<<sg/sf+r;
    return 0;
}
}
int main(){return asbt::main();}

S. [JOI Open 2016] 摩天大楼 / Skyscraper

首先将 \(a\) 排序,然后可以连续段 DP,设 \(f_{i,j,k,d}\) 表示前 \(i\) 个数连成 \(j\) 段,和为 \(k\),当前确定了 \(d\) 个边界的方案数。

一个比较直观的费用提前计算是,在插入每个数时预先算出比它大的数放在它旁边时它自己的贡献。举个例子,对于另成一段而不贴边界的转移,有:

\[f_{i+1,j+1,k-2a_{i+1},d}\leftarrow f_{i,j,k,d}\times(j+1-d) \]

其他转移是类似的。这样的时空复杂度是无法接受的,因为转移过程中 \(k\) 有增有减,虽然最后我们统计答案时只需要 \(k\in[0,L]\) 的贡献,转移过程中确是无法舍去一些 \(k\) 较大的状态的。我们希望换一种贡献的计算方法,使得转移过程中 \(k\) 只增不减。

考虑排序后的 \(a\) 数组中的相邻两项 \(a_i\)\(a_{i+1}\) 的贡献,它们的贡献即为 \((a_{i+1}-a_i)\times cnt_i\),其中 \(cnt_i\) 为覆盖了 \(a_i\)\(a_{i+1}\) 这一段的最终在题目所求排列里相邻的数对数量。考虑插入 \(a_{i+1}\),此时会对 \(cnt_i\) 产生贡献。因为插入 \(a_{i+1}\) 之前每一段的两端的数都 \(<a_{i+1}\),后面插入的数都 \(>a_{i+1}\),所以会产生的贡献就是 \(cnt_i=2j-d\)。于是新的贡献和 \(k'=k+(a_{i+1}-a_i)(2j-d)\)。于是就可以 DP 了,时间复杂度 \(O(n^2L)\)

Code
#include<bits/stdc++.h>
#define ll long long
#define il inline
using namespace std;
namespace asbt{
const int mod=1e9+7;
int n,m,a[105],f[105][105][1005][3];
int main(){
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	if(n==1){
		cout<<1;
		return 0;
	}
	sort(a+1,a+n+1);
	f[0][0][0][0]=1;
	for(int i=0;i<n;i++){
		for(int j=0;j<=i;j++){
			for(int k=0;k<=m;k++){
				for(int d=0;d<=2;d++){
					int p=k+(a[i+1]-a[i])*(2*j-d),t=f[i][j][k][d];
					if(!t||p>m){
						continue;
					}
					f[i+1][j+1][p][d]=(f[i+1][j+1][p][d]+t*1ll*(j+1-d))%mod;
					if(d<2){
						f[i+1][j+1][p][d+1]=(f[i+1][j+1][p][d+1]+t*1ll*(2-d))%mod;
					}
					if(j){
						f[i+1][j][p][d]=(f[i+1][j][p][d]+t*1ll*(2*j-d))%mod;
					}
					if(j&&d<2){
						f[i+1][j][p][d+1]=(f[i+1][j][p][d+1]+t*1ll*(2-d))%mod;
					}
					if(j>=2){
						f[i+1][j-1][p][d]=(f[i+1][j-1][p][d]+t*1ll*(j-1))%mod;
					}
				}
			}
		}
	}
	int ans=0;
	for(int i=0;i<=m;i++){
		(ans+=f[n][1][i][2])%=mod;
	}
	cout<<ans;
	return 0;
}
}
int main(){return asbt::main();}
posted @ 2025-08-09 21:42  zhangxy__hp  阅读(26)  评论(0)    收藏  举报