省选集训 12 - DP 专题

[CF1810G] The Maximum Prefix

从后往前求最大前缀和是容易的,加一个数 \(a_i\),最大前缀和 \(s\leftarrow \max(0,s+a_i)\)

所以可以从前往后定义 \(f_{i,j}\) 表示加到 \(i\),最大前缀和为 \(j\) 的概率。

当初值设为 \(f_{l+1,0}=1\) 时,长度 \(l\) 的答案即为 \(\sum_{i=0}^{l} f_{1,i}\times h_i\)

然后发现对于不同的长度,除初值外转移都是一样的,所以可以反推答案贡献。

\(g_{1,i}=h_i\),则有转移 \(g_{i,j}=g_{i-1,j+1}\times p_i+g_{i-1,\max(j-1,0)}\times (1-p_i)\)\(g_{l+1,0}\) 即为长度 \(l\) 的答案。

另一种方法,我们如果令最大前缀和为 \(x\),去算最大前缀和为 \(x\) 的贡献也是容易的。

\(f_{i,j,0/1}\) 表示枚举到 \(i\),与目标差值为 \(j\),是否达到过目标的答案,发现对于所有 \(x\) 转移是相同的。

所以令 \(f_{0,j,0/1}=h_j\),就可以一并得出所有的答案了,代码写的是第一种方法。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=5005,mod=1000000007;
int t,n,p[N],h[N],dp[N][N];
int quick_pow(int x,int y,int res=1){
	for(;y;x=x*x%mod,y>>=1)  if(y&1)  res=res*x%mod;
	return res;
}
void solve(){
	cin>>n;
	for(int i=1,x,y;i<=n;i++){
		cin>>x>>y;
		p[i]=x*quick_pow(y,mod-2)%mod;
	}
	for(int i=0;i<=n;i++)  cin>>dp[1][i];
	for(int i=2;i<=n+1;i++)
		for(int j=0;j<=n-i+1;j++)
			dp[i][j]=(dp[i-1][j+1]*p[i-1]+dp[i-1][max(j-1,0ll)]*(mod+1-p[i-1]))%mod;
	for(int i=1;i<=n;i++)  cout<<dp[i+1][0]<<(i==n?"\n":" ");
}
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr),cout.tie(nullptr);
	cin>>t;while(t--)  solve();
}

[AGC061C] First Come First Serve

去重之后理论只有两种选择:选 \(a_i\),或者在 \((a_i,b_i)\) 中有数被选择时选 \(b_i\)

考虑容斥,三种情况:选 \(a_i\) 和选 \(b_i\) 系数为 \(1\),选 \(b_i\)\((a_i,b_i)\) 中没数被选择时系数为 \(-1\)

\(dp_i\) 表示选完 \(a/b_{1,2,\cdots,i}\) 的答案,则系数为 \(1\) 的总贡献为 \(2\times dp_{i-1}\),系数 \(-1\) 的贡献双指针维护即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 500005
#define int long long
vector<int> v[N];
const int mod=998244353;
int n,a[N],b[N],l[N],r[N],dp[N];
signed main(){
	scanf("%lld",&n),dp[0]=1;
	for(int i=1;i<=n;i++)  scanf("%lld%lld",a+i,b+i);
	for(int i=1;i<=n;i++){
		l[i]=l[i-1];
		while(l[i]<n&&b[l[i]+1]<a[i])  l[i]++;
	}
	for(int i=1;i<=n;i++){
		r[i]=r[i-1];
		while(r[i]<n&&a[r[i]+1]<b[i])  r[i]++;
	}
	for(int i=1;i<=n;i++)  v[r[i]].push_back(l[i]);
	for(int i=1;i<=n;i++){
		dp[i]=dp[i-1]*2%mod;
		for(auto x:v[i])  dp[i]=(dp[i]-dp[x]+mod)%mod;
	}
	printf("%lld\n",dp[n]);
}

[ABC290Ex] Bow Meow Optimization

首先一定有序列左边的 \(\lfloor\frac{n}{2}\rfloor+\lfloor\frac{m}{2}\rfloor\) 只动物一定有 \(\lfloor\frac{n}{2}\rfloor\) 只狗和 \(\lfloor\frac{m}{2}\rfloor\) 只猫,右边同理。

如果 \(n\)\(m\) 是奇数,就把权值最大的那只放中间。

正确性显然,如果左边多一只猫,将左半边最右边的猫和右半边最左边的狗交换位置一定不劣。

然后题目就变成将 \(n-(n\&1)\) 个红色的球和 \(m-(m\&1)\) 个蓝色的球填进两个长度相同的序列。

若序列中某个球前面有 \(x\) 个与之异色的球,则权值为 \(a_i/b_i\times [2x(+1)]\),是否 \(+1\)\(n\)\(m\) 的奇偶性决定。

然后我们将两种颜色的球混在一起按权值从大到小排序。

因为权值大的一定尽量往前放,所以令 \(f_{i,j,k}\) 表示前 \(i\) 个球在第一个序列放 \(j\) 的红球和 \(k\) 个蓝球,转移容易。

现在时间复杂度为 \(O(n^3)\),已经可以通过,但是仍然可以进行优化。

给出结论:将红色的前 \(\lfloor\frac{n}{2}\rfloor\) 个放在第一个序列,蓝色的前 \(\lfloor\frac{m}{2}\rfloor\) 个放在第二个序列,一定最优。

证明:考虑权值只有 \(0\)\(1\) 的情况,如果红球有个 \(0\) 在左半边,有个 \(1\) 在右半边,交换显然不劣。

对于每个 \(x\),将大于 \(x\) 的数看成 \(1\),小于 \(x\) 的数看成 \(0\)

原序列答案一定大于等于所有 \(x\) 的答案相加,但是对于每个 \(x\) 都有同样的策略,所以等号成立,即结论成立。

然后就可以直接排序后类归并计算结果了,复杂度 \(O(n\log n)\)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 305
#define int long long
int n,m,ans,sua,sub,al,ar,bl,br,a[N],b[N];
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr),cout.tie(nullptr);
	cin>>n>>m;
	for(int i=1;i<=n;i++)  cin>>a[i],sua+=a[i];
	for(int i=1;i<=m;i++)  cin>>b[i],sub+=b[i];
	sort(a+1,a+n+1),sort(b+1,b+m+1);
	ans+=(n&1)*sub,n-=(n&1),ans+=(m&1)*sua,m-=(m&1);
	for(int k=1,i=n,j=m;k<=n+m;k++){
		if(i&&a[i]>b[j]){
			if(al<n/2)  ans+=a[i--]*bl*2,al++;
			else  ans+=a[i--]*br*2,ar++;
		}
		else{
			if(br<m/2)  ans+=b[j--]*ar*2,br++;
			else  ans+=b[j--]*al*2,bl++;
		}
	}
	cout<<ans<<"\n";
}

[CSA Round 70] Squared Ends

斜优板题,不过只能李超线段树,不能单调队列优化。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 10005
#define int long long
#define INF 0x3f3f3f3f3f3f3f3f
struct Lichao_segment_tree{
	struct line{int k,b;}l[N];
	int cnt=0,lcnt=0,rt=0,tr[N<<6],ls[N<<6],rs[N<<6];
	void init(){
		fill(tr+1,tr+cnt+1,0),fill(ls+1,ls+cnt+1,0);
		fill(rs+1,rs+cnt+1,0),cnt=lcnt=rt=0;
	}
	int calc(int num,int x){return l[num].k*x+l[num].b;}
	void upd(int num,int l,int r,int &p){
		if(!p)  p=++cnt;
		int mid=(l+r)>>1;
		if(calc(num,mid)<calc(tr[p],mid))  swap(num,tr[p]);
		if(calc(num,l)<calc(tr[p],l))  upd(num,l,mid,ls[p]);
		if(calc(num,r)<calc(tr[p],r))  upd(num,mid+1,r,rs[p]);
	}
	void insert(int k,int b){l[++lcnt]={k,b},upd(lcnt,1,1000000,rt);}
    int query(int x,int l,int r,int p){
        if(x<l||r<x||!p)  return INF;
        int mid=(l+r)>>1,sum=calc(tr[p],x);
        if(l==r)  return sum;
        return min({sum,query(x,l,mid,ls[p]),query(x,mid+1,r,rs[p])});
    }
}LCT;
int n,k,a[N],dp[105][N];
signed main(){
	ios::sync_with_stdio(false),cin.tie(nullptr),cout.tie(nullptr);
	cin>>n>>k,LCT.l[0]={0,INF};
	for(int i=1;i<=n;i++)  cin>>a[i];
	fill(dp[0],dp[0]+n+1,INF),dp[0][0]=0;
	for(int i=1;i<=k;i++,LCT.init())  for(int j=1;j<=n;j++){
		LCT.insert(-2*a[j],dp[i-1][j-1]+a[j]*a[j]);
		dp[i][j]=LCT.query(a[j],1,1000000,LCT.rt)+a[j]*a[j];
	}
	cout<<dp[k][n]<<"\n";
}

[CF1175G] Yet Another Partiton Problem

参考题解:https://www.luogu.com.cn/article/en3sau1q

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 20005
#define int long long
#define INF 0x3f3f3f3f3f3f3f3f
#define chkmin(a,b) (a)=min((a),(b))
int n,m,tot,a[N],f[N],g[N],mx[N];
struct line{
	int k,b;
	int calc(int x){return k*x+b;}
	line(int _k=0,int _b=0):k(_k),b(_b){}
}st[N];
double calc(line x,line y){return 1.0*(x.b-y.b)/(y.k-x.k);}
void insert(line x){
	if(tot&&st[tot].k==x.k){
		if(st[tot].b<=x.b)  return;
		tot--;
	}
	while(tot>1&&calc(st[tot-1],st[tot])>=calc(st[tot],x))  --tot;
	st[++tot]=x;
}
int query(int x){
	while(tot>1&&calc(st[tot-1],st[tot])>=x)  tot--;
	return tot?st[tot].calc(x):INF;
}
void solve(int l,int r){
	if(l==r)  return;
	int mid=(l+r)>>1;mx[mid]=0,mx[mid+1]=a[mid+1];
	for(int i=mid-1;i>=l;i--)  mx[i]=max(mx[i+1],a[i+1]);
	for(int i=mid+2;i<=r;i++)  mx[i]=max(mx[i-1],a[i]);
	tot=0;
	for(int i=mid+1,j=mid;i<=r;i++){
		for(;j>=l&&mx[j]<=mx[i];j--)  insert({j,g[j]});
		chkmin(f[i],query(-mx[i])+i*mx[i]);
	}
	tot=0;
	for(int i=r,j=l;i>=mid+1;i--){
		for(;j<=mid&&mx[j]>mx[i];j++)  insert({mx[j],g[j]-mx[j]*j});
		chkmin(f[i],query(i));
	}
	solve(l,mid),solve(mid+1,r);
}
signed main(){
	ios::sync_with_stdio(false),cin.tie(nullptr),cout.tie(nullptr);
	cin>>n>>m;
	for(int i=1;i<=n;i++)  cin>>a[i],f[i]=max(f[i-1],a[i]);
	for(int i=1;i<=n;i++)  f[i]*=i;
	for(int i=2;i<=m;i++)  copy(f,f+n+1,g),fill(f,f+n+1,INF),solve(0,n);
	cout<<f[n]<<"\n";
}

[JOIST 2023] 合唱 / Chorus

WQS 二分 + 斜率优化/决策单调性,一道这方面的算板子题吧。

贡献函数:令 \(c_i\) 表示第 \(i\) 个 A 前 B 的个数,有 \(w(l,r)=\sum_{i=l}^r\max(c_i-l+1,0)\)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 2000005
#define int long long
char ch[N];
deque<int> dq;
int n,k,cta,ctb,a[N],b[N],c[N],p[N],sum[N],pos[N],dp[N],ct[N],lt[N],rt[N];
int calc(int r,int l){return dp[l]+c[r]-c[p[l]-1]-l*(r-p[l]+1);}
void check(int mid){
    while(!dq.empty())  dq.pop_back();lt[0]=1,rt[0]=n,dq.push_back(dp[0]=0);
    for(int i=1;i<=n;i++){
        while(!dq.empty()&&rt[dq.front()]<i)  dq.pop_front();
        if(!dq.empty())  lt[dq.front()]=i;
        dp[i]=calc(i,dq.front())+mid,ct[i]=ct[dq.front()]+1;
        while(!dq.empty()&&calc(lt[dq.back()],i)<calc(lt[dq.back()],dq.back()))  dq.pop_back();
        if(dq.empty())  lt[i]=i,rt[i]=n,dq.push_back(i);
        else if(calc(rt[dq.back()],i)<calc(rt[dq.back()],dq.back())){
            int l=lt[dq.back()],r=rt[dq.back()];
            while(l<r){
                int mid=(l+r)>>1;
                calc(mid,i)>=calc(mid,dq.back())?l=mid+1:r=mid;
            }
            rt[dq.back()]=l-1,lt[i]=l,rt[i]=n,dq.push_back(i);
        }
        else if(rt[dq.back()]!=n)  lt[i]=rt[dq.back()]+1,rt[i]=n,dq.push_back(i);
    }
}
signed main(){
	scanf("%lld%lld%s",&n,&k,ch+1);
	for(int i=1;i<=(n<<1);i++){
		if(ch[i]=='A')  a[++cta]=i;
		else  b[++ctb]=i;
	}
	for(int i=1,j=0;i<=n;i++){
		while(j<n&&a[i]>b[j+1])  j++;
		c[i]=j;
	}
	for(int i=1,j=0;i<=n;i++){
		while(j<=n&&i>=c[j])  j++;
		p[i]=max(i+1,j);
	}
	partial_sum(c+1,c+n+1,c+1);
	int l=0,r=1e12;
	while(l<r){
		int mid=(l+r)>>1;
		check(mid),ct[n]<=k?r=mid:l=mid+1;
	}
	check(l),printf("%lld\n",dp[n]-k*l);
}

[USACO23FEB] Watching Cowflix P

因为最开始就想到了虚树,所以最后还是写的虚树做法,而没有使用更大众的根号分治。

一个必须观察到的地方是如果两个关键点的距离不超过 \(k\),那么合并起来一定不劣。

所以我们考虑随时合并距离不超过 \(k\) 的关键点,那任意时刻关键点数不超过 \(\frac{n}{k}\)

容易根据调和级数得出这样做的时间复杂度是 \(O(n\log n)\)

考虑过程怎么处理,发现当一个点相连的所有边都被同一个连通块包含时这个点就没用了。

所以我们考虑预处理每个点什么时候会变成没有用的点,再动态删除就可以了。

直接做不方便,考虑把每条边的信息拆到它对应的儿子节点上。

\(tim_i\) 表示一个点的父亲边被合并进某个关键点连通块的最快时间。

此时有:点 \(i\) 无用时 \(k\geq \max(tim_i,\max_{x\in son_i}tim_x)\)

然后 \(tim_i\) 是通过平凡树形 DP 好维护的,有用点的动态维护用 set 即可,具体细节看代码。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 200005
#define mp make_pair
#define int long long
#define INF 0x3f3f3f3f3f3f3f3f
#define chkmin(x,y) (x)=min((x),(y))
#define chkmax(x,y) (x)=max((x),(y))
string s;
vector<int> v[N],vc[N],er[N];
int n,k,rt,tot,dep[N],dfn[N];
int mnt[N],mxt[N],tim[N],dis[N],sis[N];
int sud,sue,ans[N],dot[N],edg[N],dp[N][2];
int cnt,a[N],key[N],vis[N],fa[N],st[N],val[N];
struct Lowest_common_ancestor{
	int sz[N],pre[N],son[N],top[N];
	void dfs1(int x,int fa){
		sz[x]=1,pre[x]=fa,dep[x]=dep[fa]+1;
		for(auto y:v[x]){
			if(y==fa)  continue;
			dfs1(y,x),sz[x]+=sz[y];
			if(sz[y]>sz[son[x]]) son[x]=y;
		}
	}
	void dfs2(int x,int fa){
		dfn[x]=++cnt;
		if(son[x])  top[son[x]]=top[x],dfs2(son[x],x);
		for(auto y:v[x])
			if(y!=fa&&y!=son[x])
				top[y]=y,dfs2(y,x);
	}
	void init(){dfs1(rt,0),top[rt]=rt,dfs2(rt,0);}
	int query(int x,int y){
		while(top[x]!=top[y]){
			if(dep[top[x]]>dep[top[y]]) swap(x,y);
			y=pre[top[y]];
		}
		return dep[x]<dep[y]?x:y;
	}
}LCA;
void add(int x,int y){
	fa[y]=x,vis[x]=vis[y]=1;
	vc[x].push_back(y),val[y]=dep[y]-dep[x];
}
void dfs1(int x){
	dis[x]=sis[x]=(!key[x])*INF;
	for(auto y:vc[x]){
		dfs1(y);int tmp=dis[y]+val[y];
		if(tmp<dis[x])  sis[x]=dis[x],dis[x]=tmp;
		else  sis[x]=min(sis[x],tmp);
	}
}
void dfs2(int x,int fds){
	tim[x]=dis[x]+fds;
	for(auto y:vc[x]){
		int tmp=(dis[y]+val[y]==dis[x])?sis[x]:dis[x];
		dfs2(y,min(fds,tmp)+val[y]);
	}
}
struct Node{
	int x;
	friend bool operator<(Node a,Node b){
		return mp(dep[a.x],a.x)>mp(dep[b.x],b.x);
	}
};
set<Node> lst;
signed main(){
	ios::sync_with_stdio(false),cin.tie(nullptr),cout.tie(nullptr);
	cin>>n>>s,s=' '+s;
	for(int i=1,x,y;i<n;i++){
		cin>>x>>y;
		v[x].push_back(y),v[y].push_back(x);
	}
	for(int i=1;i<=n;i++)  if(s[i]=='1')  key[i]=1,a[++k]=i,rt=i;
	LCA.init(),sort(a+1,a+k+1,[&](int x,int y){return dfn[x]<dfn[y];});
	st[++tot]=a[1];
	for(int i=2;i<=k;i++){
		int lca=LCA.query(a[i],st[tot]);  
		while(tot>1&&dep[st[tot-1]]>=dep[lca])
			add(st[tot-1],st[tot]),tot--;
		if(lca!=st[tot])  add(lca,st[tot]),st[tot]=lca;
		st[++tot]=a[i];
	}
	while(tot>1)  add(st[tot-1],st[tot]),tot--;
	dfs1(rt),dfs2(rt,0);
	for(int i=1;i<=n;i++)  if(vis[i]){
		lst.insert({i}),mnt[i]=(!key[i])*tim[i],mxt[i]=tim[i];
		for(auto x:vc[i])  chkmin(mnt[i],tim[x]),chkmax(mxt[i],tim[x]);
		er[mxt[i]].push_back(i),dot[mnt[i]]++;
		if(i!=rt)  dot[tim[i]]+=val[i]-1,edg[tim[i]]+=val[i];
	}
	sud=dot[0],sue=0;
	for(int k=1;k<=n;k++){
		for(auto x:er[k])  lst.erase({x});
		sud+=dot[k],sue+=edg[k],ans[k]=k*(sud-sue)+sud;
		for(auto it:lst){
			int y=it.x,x=fa[y];
			if(mnt[y]<=k)  dp[y][0]=INF;
			else  dp[y][1]+=k+1;
			if(tim[y]<=k)  ans[k]+=dp[y][1];
			else{
				dp[x][0]+=min(dp[y][0],dp[y][1]);
				dp[x][1]+=min(dp[y][0],dp[y][1]+min(val[y]-k-1,0ll));
			}
			dp[y][0]=dp[y][1]=0;
		}
	}
	for(int i=1;i<=n;i++)  cout<<ans[i]<<"\n";
}

[JOIST 2023] 议会 / Council

去掉 \(i\) 过后肯定票大于 \(\lfloor\frac{n}{2}\rfloor\) 的一定通过,小于的一定不通过,所以只用考虑等于的情况。

\(i\) 投肯定票的提案状压成 \(s_i\) 表示,令去掉当前 \(i\) 等于 \(\lfloor\frac{n}{2}\rfloor\) 的提案集合为 \(x\)

显然应当让 \(\text{popcount}(x\&s_j)\) 最小的 \(j\) 当副主席,二进制分块处理前后缀即可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define INF 0x3f3f3f3f
#define pop(x) __builtin_popcount(x)
#define chkmin(x,y) (x)=min((x),(y))
const int M=1024,N=300005;
int n,m,bs[N],ans[N],cnt[M],tot[M],sum[M][M];
void insert(int x){
	int hig=(x>>10),sht=(x&1023);
	for(int i=0;i<M;i++)
		chkmin(sum[hig][i],cnt[i&sht]);
}
int query(int x){
	int res=INF,hig=(x>>10),sht=(x&1023);
	for(int i=0;i<M;i++)
		chkmin(res,sum[i][sht]+cnt[i&hig]);
	return res;
}
int main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr),cout.tie(nullptr);
	cin>>n>>m,memset(sum,0x3f,sizeof sum);
	for(int i=0;i<M;i++)  cnt[i]=pop(i);
	for(int i=1;i<=n;i++)
		for(int j=0,x;j<m;j++)
			cin>>x,bs[i]|=(x<<j),tot[j]+=x;
	for(int i=2,que,res;i<=n;i++){
		insert(bs[i-1]),que=0,res=0;
		for(int j=0;j<m;j++){
			int tmp=tot[j]-(bs[i]>>j&1);
			if(tmp>=n/2)  res++;
			if(tmp==n/2)  que|=(1<<j);	
		}
		ans[i]=max(ans[i],res-query(que));
	}
	memset(sum,0x3f,sizeof sum);
	for(int i=n-1,que,res;i>=1;i--){
		insert(bs[i+1]),que=0,res=0;
		for(int j=0;j<m;j++){
			int tmp=tot[j]-(bs[i]>>j&1);
			if(tmp>=n/2)  res++;
			if(tmp==n/2)  que|=(1<<j);			
		}
		ans[i]=max(ans[i],res-query(que));
	}
	for(int i=1;i<=n;i++)  cout<<ans[i]<<"\n";
}

[AGC039E] Pairing Points

\(n=2N\),先将 \(1\) 与任意一个点 \(k\) 连上,然后序列就被分为了 \([2,k)\)\((k,n]\) 两部分。

由于两部分一定会有边跨过 \(k\),所以考虑枚举其中一条边 \((i,j)\)

但是直接枚举会算重,所以考虑让 \(i\) 为所有这些边最靠近 \(2\) 的端点,\(j\) 为最靠近 \(n\) 的端点。

为什么 \((i,j)\) 一定是一条边?

假设 \(i\) 不是和 \(j\) 连边,而是和 \(q\) 连边,\(j\) 不是和 \(i\) 连边,而是和 \(p\) 连边。

这种情况下 \((i,q)\)\((p,j)\)\((1,k)\) 一定两两互相有交,所以不可能存在,即 \((i,j)\) 有连边。

考虑 \((i,k)\) 这段区间,一定能划分出 \((i,p]\)\((p,k)\) 两段,使得 \((i,p]\)\(i\) 连通,\((p,k)\)\(k\) 相通。

为什么一定存在这样的 \(p\)

假设有边 \((i,x)\)\((y,k)\) 满足 \(x>y\),则 \((i,x)\)\((y,k)\)\((1,k)\)\((i,j)\) 一定能构成一个环。

所以我们枚举分界点 \(p\),并在 \((k,j)\) 中间同理枚举 \(q\)

可以发现 \((i,p)\)\((p+1,q-1)\)\((q,j)\) 分别构成了完全相同的子问题。

\(f_{l,r,k}\) 表示 \([l,k)\)\((k,r]\) 有相连边跨过 \((k,?)\) 的方案数。

类比前面 \(1\) 的分析,可以得知我们是不在乎 \(k\) 具体和谁连边的,所以有答案为 \(\sum_{k=2}^{n}f_{2,n,k}\)

每个 \((l,r,k)\) 枚举 \(i\)\(j\)\(p\)\(q\) 的答案为 \(f_{l,p,i}\times f_{p+1,q-1,k}\times f_{q,r,j}\),记忆化搜索或直接 DP 均可。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
char ch[45][45];
int n,ans,dp[45][45][45];
int dfs(int l,int r,int k){
	int &res=dp[l][r][k];
	if(res>=0)  return res;
	if(l==k||k==r)  return res=(l==k&&k==r);
	res=0;
	for(int i=l;i<k;i++)  for(int j=k+1;j<=r;j++){
		if(ch[i][j]=='0')  continue;
		for(int p=i;p<k;p++)  for(int q=k+1;q<=j;q++)
			res+=dfs(l,p,i)*dfs(q,r,j)*dfs(p+1,q-1,k);
	}
	return res;
}
signed main(){
	ios::sync_with_stdio(false),cin.tie(nullptr),cout.tie(nullptr);
	cin>>n,n<<=1;
	for(int i=1;i<=n;i++){
		cin>>ch[i],copy(ch[i],ch[i]+n,ch[i]+1);
		for(int j=1;j<=n;j++)  fill(dp[i][j],dp[i][j]+n+1,-1);
	}
	for(int i=2;i<=n;i++)  if(ch[1][i]=='1')  ans+=dfs(2,n,i);
	cout<<ans<<"\n";
}

[CF1060F] Shrinking Tree

参考题解:https://www.luogu.com.cn/article/9ryp6q1b

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=55;
int n,sz[N];
vector<int> v[N];
double fac[N],g[N],tmp[N],C[N][N],dp[N][N];
void dfs(int x,int fa){
	sz[x]=1,dp[x][0]=1;
	for(auto y:v[x]){
		if(y==fa)  continue;
		dfs(y,x),memset(g,0,sizeof g);
		for(int i=0;i<=sz[y];i++){
			for(int j=1;j<=sz[y];j++){
				if(j>i)  g[i]+=dp[y][i];
				else  g[i]+=0.5*dp[y][j-1];
			}
		}
		memset(tmp,0,sizeof tmp);
		for(int i=sz[x]-1;i>=0;i--)
			for(int j=sz[y];j>=0;j--)
				tmp[i+j]+=dp[x][i]*g[j]*C[i+j][j]*C[sz[x]-i-1+sz[y]-j][sz[x]-i-1];
		sz[x]+=sz[y],copy(tmp,tmp+sz[x],dp[x]);
	}
}
int main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr),cout.tie(nullptr);
	cin>>n,fac[0]=1;
	for(int i=0;i<=n;i++)  C[i][0]=1;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=i;j++)
			C[i][j]=C[i-1][j]+C[i-1][j-1];
	for(int i=1;i<=n;i++)  fac[i]=fac[i-1]*i;
	for(int i=1,x,y;i<n;i++){
		cin>>x>>y;
		v[x].push_back(y),v[y].push_back(x);
	}
	for(int i=1;i<=n;i++){
		memset(dp,0,sizeof dp),dfs(i,0);
		cout<<dp[i][n-1]/fac[n-1]<<"\n";
	}
}
posted @ 2026-01-12 22:15  tkdqmx  阅读(49)  评论(1)    收藏  举报