2024.4 总结

数据结构

【ABC348G】Max (Sum - Max)

题目描述

给出 \(n(1 \le n \le 10^5)\) 以及两个长度为 \(n\) 的数组 \(a,b\) ,对于 \(1 \le k \le n\) ,从 \(1\)\(n\) 中选出一个包含 \(k\) 个数的集合 \(S\) ,使得 \(\sum_{i\in S} a_i -max_{i\in S}b_i\) 最大。

解题思路

对于 \(1 \le k \le n\),我们设 \(t_k\) 为答案的 \(S\) 的最大 \(b_i\) 所在的位置,一定有决策单调性,即 \(t_{k-1} \le t_k\)
证明很简单,如果 \(t_{k-1} \ge t_k\) 的话,那么 \(t_k=t_{k-1}\) 肯定更优。
那么我们可以考虑分治,设分治区间为 \([l,r]\) ,每次枚举 \(mid\) 在当前可选决策点范围中哪个最优,然后分治,小于 \(mid\) 的可选决策点范围小于 \(t_{mid}\) ,大于 \(mid\) 的范围大于 \(t_{mid}\)
区间查询前 \(k\) 大和用主席树做,时间复杂度 \(O(nlog^2n)\)

Code

#include<bits/stdc++.h>
using namespace std;
struct datay
{
	long long x,y;
}a[200005];
struct data
{
	int lc,rc,cnt;
	long long v;
}f[7000005];
int n,root[2000005],num;
long long d[200005],ans[500005],v[500005];
bool cmp(datay q,datay w)
{
	return q.y<w.y;
}
bool cmp1(datay q,datay w)
{
	return q.x<w.x;
}
int build(int l,int r)
{
	int q=++num,mid=(l+r)>>1;
	if(l==r)return q;
	f[q].lc=build(l,mid),f[q].rc=build(mid+1,r);
	return q;
}
int dijah(int x,int l,int r,int k,long long v)
{
	if(l==r)
	{
		f[++num]=f[x],f[num].cnt++,f[num].v+=v;
		return num;
	}
	int p=++num,mid=(l+r)>>1;f[p]=f[x];
	if(k<=mid)f[p].lc=dijah(f[x].lc,l,mid,k,v);
	else f[p].rc=dijah(f[x].rc,mid+1,r,k,v);
	f[p].v=f[f[p].lc].v+f[f[p].rc].v,f[p].cnt=f[f[p].lc].cnt+f[f[p].rc].cnt;
	return p;
}
long long gaia(int x1,int x2,int l,int r,int k)
{
	if(k==0)return 0;
	if(l==r)return f[x2].v-f[x1].v;
	int mid=(l+r)>>1;
	if(f[f[x2].rc].cnt-f[f[x1].rc].cnt>k)return gaia(f[x1].rc,f[x2].rc,mid+1,r,k);
	return gaia(f[x1].lc,f[x2].lc,l,mid,k-f[f[x2].rc].cnt+f[f[x1].rc].cnt)+f[f[x2].rc].v-f[f[x1].rc].v;
}
void solve(int l,int r,int L,int R)
{
	if(l>r)return;
	long long mid=(l+r)>>1,q;
	for(int i=max(L,int(mid));i<=R;i++)q=gaia(root[0],root[i-1],1,n,mid-1)+d[a[i].x]-a[i].y,ans[mid]=(q>v[mid])?i:ans[mid],v[mid]=max(v[mid],q);
	solve(l,mid-1,L,ans[mid]),solve(mid+1,r,ans[mid],R);
	return;
}
int main()
{
	memset(v,0x80,sizeof(v));
	int g=0;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%lld%lld",&a[i].x,&a[i].y);
	sort(a+1,a+n+1,cmp1);
	for(int i=1;i<=n;i++)d[i]=a[i].x,a[i].x=i;
	sort(a+1,a+n+1,cmp);
	root[0]=build(1,n);
	for(int i=1;i<=n;i++)root[i]=dijah(root[i-1],1,n,a[i].x,d[a[i].x]);
	solve(1,n,1,n);
	for(int i=1;i<=n;i++)printf("%lld\n",v[i]);

  return 0;
}

分治

【CF97B】 Superset

题目描述

给出 \(n\) 个点,添加一些点,使得每两个点在同一行,在同一列,或以这两个点为顶点的长方形包含(在内部或边缘)其他的至少一个点,新增的点数小于 $2 \times 10^5 $ 。

解题思路

因为有点数的限制,考虑分治。
\(x\) 坐标排序,那么,处于 \(mid\) 上面的点与 \(mid\) 下面的点要相连,可以再 \(x=x_{mid}\) 的位置加点,使得每个点都在 \(x=x_{mid}\) 上有一个点 \(y\) 与其相等。
时间复杂度 \(O(nlogn)\)

Code

#include<bits/stdc++.h>
using namespace std;
struct datay
{
	int x,y;
}a[5000005];
int n;
bool cmp(datay q,datay w)
{
	if(q.x!=w.x)return q.x<w.x;
	return q.y<w.y;
}
void solve(int l,int r)
{
	if(l==r)return;
	int mid=(l+r)>>1;
	for(int i=l;i<=r;i++)a[++n].x=a[mid].x,a[n].y=a[i].y;
	solve(l,mid),solve(mid+1,r);
	return;
}
int main()
{
	int s=0;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%d%d",&a[i].x,&a[i].y);
	sort(a+1,a+n+1,cmp),solve(1,n); 
	sort(a+1,a+n+1,cmp),a[0].x=a[0].y=-1e9;
	for(int i=1;i<=n;i++)if(a[i].x!=a[i-1].x||a[i].y!=a[i-1].y)s++;
	printf("%d\n",s);
	for(int i=1;i<=n;i++)if(a[i].x!=a[i-1].x||a[i].y!=a[i-1].y)printf("%d %d\n",a[i].x,a[i].y);


  return 0;
}

【CF429D】 Tricky Function

题目描述

给一个长度为 \(n\) 的序列,求贡献最小的 \((i,j),i < j\) ,使得 $(i-j)^2 +(\sum_{u=i+1}^{j} a_u)^2 $ 最小,\(1 \le n \le 10^5\)

题目描述

\(b_i=\sum_{j=1}^i a_i\) ,那么这题要求的就是平面最近点对。
分治,设当前的最优解为 \(s\) ,分治的区间为 \([l,r]\)
我们考虑枚举左边,找右边距离左边的点距离不超过 \(s\) 的点,并更新答案。
看起来每次要找很多点,但是其实最多只有 \(4\) 个。
时间复杂度为 \(O(nlogn)\)

Code

#include<bits/stdc++.h>
using namespace std;
struct datay
{
	long long x,y;
}a[400005],b[400005];
int n;
long long s;
long long dis(int x,int y)
{
	return (a[x].x-a[y].x)*(a[x].x-a[y].x)+(a[x].y-a[y].y)*(a[x].y-a[y].y);
}
void solve(int l,int r)
{
	if(l==r)return;
	int mid=(l+r)>>1,q=mid+1,w=mid,e,s1=sqrt(s)+1,po=a[mid].x;
	solve(l,mid),solve(mid+1,r);
	for(int i=mid+1;i<=r;i++)if(a[i].x-po<=s1)b[++w].x=i;
	for(int i=l;i<=mid;i++)
	{
		if(po-a[i].x>s1)continue;
		while(q<w&&a[i].y-a[b[q].x].y>s1)q++;
		for(int j=q;j<=w&&a[b[j].x].y-a[i].y<=s1;j++)s=min(s,dis(i,b[j].x));
	}
	q=e=l,w=mid+1;
	while(q<=mid&&w<=r)b[e++]=(a[q].y>a[w].y)?a[w++]:a[q++];
	while(q<=mid)b[e++]=a[q++];
	while(w<=r)b[e++]=a[w++];
	for(int i=l;i<=r;i++)a[i]=b[i];
	return;
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%lld",&a[i].y),a[i].y+=a[i-1].y,a[i].x=i;
	s=dis(1,2),solve(1,n);
	cout<<s;







  return 0;
}

图论

【CF1442C】 Graph Transpositions

题目描述

给一个 \(n\)\(m\) 边的有向图,移动一次需要 \(1s\) ,同时你可以把图每条边方向翻转,第 \(k\) 次操作需要 \(2^{k-1} s\) ,求从 \(1\)\(n\) 的最小时间,\(1 \le n \le 2 \times 10^5\)

解题思路

设最小翻转 \(x\) 次可到达 \(n\)

  • $ x \le 18$
    那么我们可以发现,\(2^{18}>n\) ,最优解翻图次数不超过 \(18\) 次,建 \(18\) 层图,每层之间代表将图翻转一次即可。
  • \(x>18\)
    那么翻 \(x\) 次一定是最优解,建两层图,把层与层之间转移的代价设成 \(1e7\) ,那么最优解的翻转次数一定是最小的。

前一种情况每层用一次 \(bfs\) ,后一种情况跑堆优化 \(dij\) ,时间复杂度 \(O(nlogn)\) ,全跑 \(spfa\) 或堆优化 \(dij\) 过不去。

Code

#include<bits/stdc++.h>
#pragma GCC optimize(1)
#pragma GCC optimize(2)
#pragma GCC optimize(3,"Ofast","inline")
using namespace std;
const long long mod=998244353;
struct datay
{
	int x,y;
}b[200005];
int n,m,p[2000005],qwe;
bool v[4500005];
long long f[4500005];
vector<int> a[4500005],t[4500005];
queue<int> l;
bool operator<(const datay &q,const datay &w)
{
	return q.y>w.y;
}
priority_queue<datay> l1;
void spfa()
{
	memset(v,false,sizeof(v));
	memset(f,0x7F,sizeof(f));
	l.push(1),v[1]=true,f[1]=0;
	int x;
	for(int i=1;i<=qwe;i++)
	{
		while(l.size())
		{
			x=l.front(),l.pop(),v[x]=false;
			for(int i=0;i<a[x].size();i++)
			{
				if(f[a[x][i]]>f[x]+t[x][i])
				{
					f[a[x][i]]=f[x]+t[x][i];
					if(!v[a[x][i]])v[a[x][i]]=true,l.push(a[x][i]);
				}
			}
		}
		if(qwe==i)break;	
		for(int j=(i-1)*n+1;j<=i*n;j++)f[j+n]=f[j]+p[i-1],v[j+n]=true,l.push(j+n);
	}
	return;
}
void dij()
{
	datay x,y;
	memset(v,false,sizeof(v));
	memset(f,0x7f,sizeof(f));
	x.x=1,x.y=0,l1.push(x),f[1]=0;
	while(l1.size())
	{
		while(l1.size()&&v[l1.top().x])l1.pop();
		if(l1.size()==0)break;
		x=l1.top(),l1.pop(),v[x.x]=true;
		for(int i=0;i<a[x.x].size();i++)
		{
			if(f[x.x]+t[x.x][i]<f[a[x.x][i]])f[a[x.x][i]]=f[x.x]+t[x.x][i],y.x=a[x.x][i],y.y=f[a[x.x][i]],l1.push(y);
		}
	} 
	return;
}
int main()
{
	int x,y;p[0]=1;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=200000;i++)p[i]=p[i-1]*2%mod;
	for(int i=1;i<=m;i++)scanf("%d%d",&b[i].x,&b[i].y);
	qwe=20;
	for(int i=1;i<=m;i++)
	{
		for(int j=1;j<=20;j++)
		{
			if(j&1)a[b[i].x+(j-1)*n].push_back(b[i].y+(j-1)*n),t[b[i].x+(j-1)*n].push_back(1);
			else a[b[i].y+(j-1)*n].push_back(b[i].x+(j-1)*n),t[b[i].y+(j-1)*n].push_back(1);
		}
	}
	spfa();
	long long s=1000000000000000005;
	for(int i=1;i<=20;i++)s=min(s,f[n*i]);
	for(int i=1;i<=n*20;i++)a[i].clear(),t[i].clear();
	for(int i=1;i<=m;i++)
	{
		for(int j=1;j<=2;j++)
		{
			if(j&1)a[b[i].x+(j-1)*n].push_back(b[i].y+(j-1)*n),t[b[i].x+(j-1)*n].push_back(1);
			else a[b[i].y+(j-1)*n].push_back(b[i].x+(j-1)*n),t[b[i].y+(j-1)*n].push_back(1);
		}
	}
	for(int i=1;i<=n;i++)a[i].push_back(n+i),t[i].push_back(10000000),a[n+i].push_back(i),t[n+i].push_back(10000000);
	dij();
	if(f[n]>f[n*2])swap(f[n],f[2*n]);
	if(s>=1e18)s=p[f[n]/10000000]+f[n]%10000000-1;
	else if(f[n]/10000000<=27)s=min(s,p[f[n]/10000000]+f[n]%10000000-1)%mod;
	cout<<s%mod;
	
  return 0;
}

【YBOJ】 Tree

题目描述

给出一棵树以及 \(n\) 个点的点权 \(p_i\) ,保证 \(\sum_{i=1}^n p_i =1\) ,每次有 \(p_i\) 的概率选中一个点,使得该点及其周围的点颜色翻转,操作 \(m\) 次,求最后黑色点构成的连通块的期望个数,\(1 \le n\le 10^4,1 \le m \le 10^9\)

解题思路

首先,仙人掌中的连通块个数为满足条件点数减去边数再加上环数,而树中没有环,我们只需要统计点数或边数即可。
考虑点的情况。
对于一个点,我们可以计算出它每次被翻转的概率,那么我们可以用 \(dp\) 的方法求出其为黑色的概率。
\(m \le 10^9\) ,所以要用矩阵加速。
对于边的情况,同理,只是边的情况有 \(4\) 种,改变的方法也有 \(4\) 种,开的矩阵为 \(4 \times 4\)
时间复杂度 \(O(nlogm)\)

Code

#include<bits/stdc++.h>
using namespace std;
const long long mod=998244353;
int n,m;
long long a[10005],l,v[10005],d[10005],s,fa[10005];
vector<int> t[10005];
long long dijah(long long x,long long y)
{
	long long h=1;
	while(y)
	{
		if(y&1)h=(h*x)%mod;
		x=(x*x)%mod,y>>=1;
	}
	return h;
}
void dfs1(int x,int y)
{
	v[x]=(v[x]+a[y])%mod,fa[x]=y;
	for(int i=0;i<t[x].size();i++)if(t[x][i]!=y)dfs1(t[x][i],x),v[x]=(v[x]+a[t[x][i]])%mod;
	return;
}
long long gaia(long long p)
{
	long long a1[2][2],a2[2][2],a3[2][2],y=m;
	memset(a1,0,sizeof(a1)),memset(a2,0,sizeof(a2)),memset(a3,0,sizeof(a3));
	a1[0][0]=a1[0][1]=1,a2[0][0]=a2[1][1]=(1-p+mod)%mod,a2[0][1]=a2[1][0]=p;
	while(y)
	{
		if(y&1)
		{
			for(int i=0;i<2;i++)
			{
				for(int j=0;j<2;j++)
				{
					a3[i][j]=0;
					for(int u=0;u<2;u++)a3[i][j]+=a1[u][j]*a2[i][u]%mod,a3[i][j]%=mod;
				}
			}
			for(int i=0;i<2;i++)
			{
				for(int j=0;j<2;j++)a1[i][j]=a3[i][j];
			}
		}
		for(int i=0;i<2;i++)
		{
			for(int j=0;j<2;j++)
			{
				a3[i][j]=0;
				for(int u=0;u<2;u++)a3[i][j]+=a2[u][j]*a2[i][u]%mod,a3[i][j]%=mod;
			}
		}
		for(int i=0;i<2;i++)
		{
			for(int j=0;j<2;j++)a2[i][j]=a3[i][j];
		}
		y>>=1;
	}
	return a1[1][1];
}
long long gaia2(long long p0,long long p1,long long p2,long long p3)
{
	long long a1[4][4],a2[4][4],a3[4][4],y=m;
	memset(a1,0,sizeof(a1)),memset(a2,0,sizeof(a2)),memset(a3,0,sizeof(a3));
	a1[0][0]=1,a2[0][0]=a2[1][1]=a2[2][2]=a2[3][3]=p0,a2[0][1]=a2[1][0]=a2[2][3]=a2[3][2]=p1,a2[0][2]=a2[1][3]=a2[2][0]=a2[3][1]=p2,a2[0][3]=a2[1][2]=a2[2][1]=a2[3][0]=p3;
	while(y)
	{
		if(y&1)
		{
			for(int i=0;i<4;i++)
			{
				for(int j=0;j<4;j++)
				{
					a3[i][j]=0;
					for(int u=0;u<4;u++)a3[i][j]+=a1[u][j]*a2[i][u]%mod,a3[i][j]%=mod;
				}
			}
			for(int i=0;i<4;i++)
			{
				for(int j=0;j<4;j++)a1[i][j]=a3[i][j];
			}
		}
		for(int i=0;i<4;i++)
		{
			for(int j=0;j<4;j++)
			{
				a3[i][j]=0;
				for(int u=0;u<4;u++)a3[i][j]+=a2[i][u]*a2[u][j]%mod,a3[i][j]%=mod;
			}
		}
		for(int i=0;i<4;i++)
		{
			for(int j=0;j<4;j++)a2[i][j]=a3[i][j];
		}
		y>>=1;
	}
	return a1[3][0];
}
int main()
{
	int x,y;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)scanf("%lld",&a[i]),l+=a[i];
	for(int i=1;i<=n;i++)a[i]=(a[i]*dijah(l,mod-2))%mod,v[i]=a[i];
	for(int i=1;i<n;i++)scanf("%d%d",&x,&y),t[x].push_back(y),t[y].push_back(x);
	dfs1(1,0);
	for(int i=1;i<=n;i++)d[i]=gaia(v[i]),s=(s+d[i])%mod;
	for(int i=2;i<=n;i++)s=(s-gaia2(((1-v[i]-v[fa[i]]+a[i]+a[fa[i]])%mod+mod)%mod,v[fa[i]]-a[i]-a[fa[i]],v[i]-a[fa[i]]-a[i],a[i]+a[fa[i]])+mod)%mod;
	cout<<(s%mod+mod)%mod;

  return 0;
}

【Luogu P7735】 轻重边

题目描述

给一棵 \(n\) 个点的树,有两个操作。

  • \(a\)\(b\) 上所有点相连的重边设为轻边,再将 \(a\)\(b\) 路径设为重边。
  • 查询 \(a\)\(b\) 的重边个数。

\(1 \le n,m \le 10^5\)\(m\) 为操作个数。

解题思路

这题可以用树剖思想,从某节点到 \(1\) 的轻边的个数不超过 \(logn\) ,每次 \(O(log^2n)\) 在树上暴力修改查询,能过,但很难打。
我们可以给每个点设一个颜色,一条重链的颜色相同,那么一条边是重边仅当两端点颜色相同。
那就很好做了,每次修改颜色,查询查连续颜色段个数即可。
时间复杂度 \(O(nlog^2n)\)

Code

#include<bits/stdc++.h>
using namespace std;
int n,m,fa[100005],size[100005],deep[100005],son[100005],top[100005],dfn[100005],num,f[400005],d[400005],ls[400005],rs[400005],vv[400005];
vector<int> a[100005];
void galaxy(int x,int l,int r,int v)
{
	f[x]=r-l,d[x]=v,ls[x]=rs[x]=v,vv[x]=v;
	return;
}
void pushdown(int x,int l,int r)
{
	if(d[x]==0)return;
	int lc=(x<<1),rc=(x<<1)|1,mid=(l+r)>>1;
	galaxy(lc,l,mid,d[x]),galaxy(rc,mid+1,r,d[x]),d[x]=0; 
	return;
}
void dijah(int x,int l,int r,int ql,int qr,int v)
{
	if(ql<=l&&r<=qr)
	{
		galaxy(x,l,r,v);
		return;
	}
	pushdown(x,l,r);
	int lc=(x<<1),rc=(x<<1)|1,mid=(l+r)>>1;
	if(ql<=mid)dijah(lc,l,mid,ql,qr,v);
	if(qr>mid)dijah(rc,mid+1,r,ql,qr,v);
	f[x]=f[lc]+f[rc]+(ls[rc]==rs[lc]),ls[x]=ls[lc],rs[x]=rs[rc];
	return;
}
int gaia(int x,int l,int r,int ql,int qr)
{
	if(ql<=l&&r<=qr)return f[x];
	pushdown(x,l,r);
	int lc=(x<<1),rc=(x<<1)|1,mid=(l+r)>>1;
	if(qr<=mid)return gaia(lc,l,mid,ql,qr);
	if(ql>mid)return gaia(rc,mid+1,r,ql,qr);
	return gaia(lc,l,mid,ql,qr)+gaia(rc,mid+1,r,ql,qr)+(rs[lc]==ls[rc]);
}
int gaia1(int x,int l,int r,int k)
{
	if(l==r)return vv[x];
	pushdown(x,l,r);
	int mid=(l+r)>>1;
	if(k<=mid)return gaia1(x<<1,l,mid,k);
	return gaia1((x<<1)|1,mid+1,r,k);
}
void dfs1(int x,int y)
{
	fa[x]=y,size[x]=1,deep[x]=deep[y]+1;
	for(int i=0;i<a[x].size();i++)
	{
		if(a[x][i]==y)continue;
		dfs1(a[x][i],x),son[x]=(size[a[x][i]]>size[son[x]])?a[x][i]:son[x],size[x]+=size[a[x][i]]; 
	}
	return;
}
void dfs2(int x,int y)
{
	dfn[x]=++num;
	if(!son[x])return;
	top[son[x]]=top[x],dfs2(son[x],x);
	for(int i=0;i<a[x].size();i++)if(a[x][i]!=y&&a[x][i]!=son[x])top[a[x][i]]=a[x][i],dfs2(a[x][i],x);
	return;
}
void modify(int x,int y,int v)
{
	while(top[x]!=top[y])
	{
		if(deep[top[x]]<deep[top[y]])swap(x,y);
		dijah(1,1,n,dfn[top[x]],dfn[x],v);
		x=fa[top[x]];
	}
	if(deep[x]>deep[y])swap(x,y);
	dijah(1,1,n,dfn[x],dfn[y],v); 
	return;
}
int query(int x,int y)
{
	int h=0;
	while(top[x]!=top[y])
	{
		if(deep[top[x]]<deep[top[y]])swap(x,y);
		h+=gaia(1,1,n,dfn[top[x]],dfn[x]);
		if(gaia1(1,1,n,dfn[top[x]])==gaia1(1,1,n,dfn[fa[top[x]]]))h++;
		x=fa[top[x]];
	}
	if(deep[x]>deep[y])swap(x,y);
	return h+gaia(1,1,n,dfn[x],dfn[y]);
}
void poi()
{
	memset(f,0,sizeof(f)),memset(d,0,sizeof(d));
	memset(ls,0,sizeof(ls)),memset(rs,0,sizeof(rs));
	memset(son,0,sizeof(son)),memset(vv,0,sizeof(vv));
	int p,x,y;
	scanf("%d%d",&n,&m),num=0;
	for(int i=1;i<=n;i++)a[i].clear(),dijah(1,1,n,i,i,i);
	for(int i=1;i<n;i++)scanf("%d%d",&x,&y),a[x].push_back(y),a[y].push_back(x); 
	dfs1(1,0),top[1]=1,dfs2(1,0);
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d%d",&p,&x,&y);
		if(p==1)modify(x,y,i+n);
		else printf("%d\n",query(x,y));
	}
	return;
}
int main()
{
	int qwe;
	scanf("%d",&qwe);
	for(int i=1;i<=qwe;i++)poi();
  return 0;
}

【Luogu P5904】 HOT-Hotels 加强版

题目描述

给出一棵有 \(n\) 个点的树,求有多少组点 \((i,j,k)\) 满足 \(i,j,k\) 两两之间的距离都相等, \(1\le n\le10^5, 1\le a\le b\le n\)

解题思路

先考虑 \(n^2\) 做法,设 \(f_{i,j}\)\(i\) 的子树中距离 \(i\)\(j\) 的节点个数,\(g_{i,j}\)\(i\) 节点还需一条长度为 \(i\) 的链就可以有贡献的情况数。
转移很简单,答案就是 \(g_{i,j} \times f_{i,j}\)
如何优化?由于只有深度信息,考虑长链剖分。
结合指针做到 \(O(n)\) 时间复杂度。

Code

#include<bits/stdc++.h>
using namespace std;
int n,d[100005],deep[100005],son[1000005];
long long *f[100005],*g[100005],qwe[3000005],s=0,*p=qwe;
vector<int> a[100005];
void dfs1(int x,int y)
{
	deep[x]=deep[y]+1;
	for(int i=0;i<a[x].size();i++)if(a[x][i]!=y)dfs1(a[x][i],x),son[x]=(d[a[x][i]]>d[son[x]])?a[x][i]:son[x];
	d[x]=d[son[x]]+1;
	return;
}
void dfs2(int x,int y)
{
	if(son[x])f[son[x]]=f[x]+1,g[son[x]]=g[x]-1,dfs2(son[x],x);
	f[x][0]=1,s+=g[x][0];
	for(int i=0;i<a[x].size();i++)
	{
		if(a[x][i]!=y&&a[x][i]!=son[x])
		{
			f[a[x][i]]=p,p+=(d[a[x][i]]<<2),g[a[x][i]]=p,p+=(d[a[x][i]]<<2);
			dfs2(a[x][i],x);
			for(int j=0;j<d[a[x][i]];j++)
			{
				if(j!=0)s+=g[a[x][i]][j]*f[x][j-1];
				s+=f[a[x][i]][j]*g[x][j+1];
			}
			for(int j=0;j<d[a[x][i]];j++)
			{
				g[x][j+1]+=f[x][j+1]*f[a[x][i]][j];
				if(j!=0)g[x][j-1]+=g[a[x][i]][j]; 
				f[x][j+1]+=f[a[x][i]][j];
			}
		}
	}
	return;
}
int main()
{
	int x,y;
	scanf("%d",&n);
	for(int i=1;i<n;i++)scanf("%d%d",&x,&y),a[x].push_back(y),a[y].push_back(x);
	dfs1(1,0);
	f[1]=p,p+=(d[1]<<2),g[1]=p,p+=(d[1]<<2);
	dfs2(1,0);
	cout<<s<<'\n';
  return 0;
}

【PKUWC2019】 你和虚树的故事弱化版

题目描述

给出一棵树,\(n\) 个点,每点有点权。
定义一种颜色的树是:把该颜色的点两两路径上的点和边都拿出来构成的图形。你可以选出 \(k\) 种颜色来,求:有多少种方案可以使每种颜色交集产生的树非空,\(1 \le k \le m \le n \le 10^5\)

解题思路

分析题目,每种情况最多一个连通块。
理解点减边模型,将每种情况的点数减去边数求和加在一起,因为不符合的情况连通块个数为 \(0\) ,符合的连通块个数为 \(1\) ,所以就是每个点被选到的方案数之和减去每条边被选到的方案数之和。
这个就很简单了,题目都提示我们用虚树了,只需要虚树思想加树上差分即可。
时间复杂度 \(O(nlogn)\),正解要用多项式,不会。

Code

#include<bits/stdc++.h>
using namespace std;
const long long mod=998244353;
long long p[100005],d1[100005],d2[100005],s;
int n,m,k,dfn[100005],num,f[100005][21],deep[100005];
vector<int> t[100005],a[100005];
long long dijah(long long x,long long y)
{
	long long h=1;
	while(y)
	{
		if(y&1)h=(h*x)%mod;
		x=(x*x)%mod,y>>=1;
	}
	return h;
}
long long C(long long x,long long y)
{
	return (p[x]*dijah(p[x-y],mod-2)%mod)*dijah(p[y],mod-2)%mod;
}
int LCA(int x,int y)
{
	if(deep[x]<deep[y])swap(x,y);
	for(int i=20;i>=0;i--)x=(deep[f[x][i]]>=deep[y])?f[x][i]:x;
	if(x==y)return x;
	for(int i=20;i>=0;i--)x=(f[x][i]!=f[y][i])?f[x][i]:x,y=(deep[x]!=deep[y])?f[y][i]:y;
	return f[x][0];
}
void dfs1(int x,int y)
{
	dfn[x]=++num,f[x][0]=y,deep[x]=deep[y]+1;
	for(int i=0;i<a[x].size();i++)if(a[x][i]!=y)dfs1(a[x][i],x);
	return;
}
void dfs2(int x,int y)
{
	for(int i=0;i<a[x].size();i++)if(a[x][i]!=y)dfs2(a[x][i],x),d1[x]+=d1[a[x][i]],d2[x]+=d2[a[x][i]]; 
	if(d1[x]>=k)s=(s+C(d1[x],k))%mod;
	if(d2[x]>=k)s=(s-C(d2[x],k)+mod)%mod;
	return;
}
int main()
{
	p[0]=1;
	for(int i=1;i<=100000;i++)p[i]=p[i-1]*i%mod;
	int x,y;
	scanf("%d%d%d",&n,&m,&k);
	for(int i=1;i<=n;i++)scanf("%d",&x),t[x].push_back(i);
	for(int i=2;i<=n;i++)scanf("%d",&x),a[x].push_back(i),a[i].push_back(x);
	dfs1(1,0);
	for(int i=1;i<=20;i++)
	{
		for(int j=1;j<=n;j++)f[j][i]=f[f[j][i-1]][i-1];
	}
	for(int i=1;i<=m;i++)
	{
		if(t[i].size()==0)continue;
		x=t[i][0],d1[t[i][0]]++,d2[t[i][0]]++;
		for(int j=1;j<t[i].size();j++)
		{
			x=LCA(x,t[i][j]);
			d1[t[i][j]]++,d1[LCA(t[i][j-1],t[i][j])]--;
			d2[t[i][j]]++,d2[LCA(t[i][j-1],t[i][j])]--;
		}
		d1[f[x][0]]--,d2[x]--;
	}
	dfs2(1,0);
	cout<<s;
	








  return 0;
}

【Luogu P3232】 游走

题目描述

给定一个 \(n\) 个点 \(m\) 条边的无向连通图,顶点从 \(1\) 编号到 \(n\),边从 \(1\) 编号到 \(m\)。小 Z 在该图上进行随机游走,初始时小 Z 在 \(1\) 号顶点,每一步小 Z 以相等的概率随机选择当前顶点的某条边,沿着这条边走到下一个顶点,获得等于这条边的编号的分数。当小 Z 到达 \(n\) 号顶点时游走结束,总分为所有获得的分数之和。 现在,请你对这 \(m\) 条边进行编号,使得小 Z 获得的总分的期望值最小,\(1 \le n \le 500\)

解题思路

我们需要计算每条边的期望经过次数,考虑计算每个点的期望经过次数 \(f_i\)
\(f_i\) 可以由周围的点转移而来,据此我们就可以列方程了,我们只需要注意两个特殊点 \(1,n\)\(f_1\)\(+1\)\(f_n\) 不能转移至其他点,用高斯消元解决即可。
每条边的期望经过个数根据两端点即可得出,贪心赋值边权即可。
时间复杂度 \(O(n^3)\)

Code

#include<bits/stdc++.h>
using namespace std;
double b[505][505],t[505],s=0,l[1000005];
int n,m,d[505];
vector<int> a[505];
int main()
{
	int x,y;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++)scanf("%d%d",&x,&y),a[x].push_back(y),a[y].push_back(x),d[x]++,d[y]++;
	for(int i=1;i<=n;i++)
	{
		for(int j=0;j<a[i].size();j++)if(a[i][j]!=n)b[i][a[i][j]]=1.000000/double(d[a[i][j]]);
		b[i][i]=-1;
	}
	b[1][n+1]=-1; 
	for(int i=1;i<=n;i++)
	{
		for(int j=i;j<=n;j++)if(abs(b[j][i])>abs(b[i][i]))swap(b[j],b[i]);
		for(int j=i+1;j<=n;j++)
		{
			double z=b[j][i]/b[i][i];
			for(int u=i;u<=n+1;u++)b[j][u]-=b[i][u]*z;
		}
	}
	for(int i=n;i>=1;i--)
	{
		for(int j=i+1;j<=n;j++)b[i][n+1]-=b[i][j]*t[j];
		t[i]=b[i][n+1]/b[i][i]; 
	}
	m=0;
	for(int i=1;i<=n;i++)
	{
		for(int j=0;j<a[i].size();j++)l[++m]=((i!=n)?t[i]/double(d[i]):0)+((a[i][j]!=n)?t[a[i][j]]/double(d[a[i][j]]):0);
	}
	sort(l+1,l+m+1);
	for(int i=1;i<=m;i+=2)s+=(l[i])*double(m/2-i/2);
	printf("%.3f",s);





  return 0;
}

【Luogu P5643】 随机游走

题目描述

给定一棵 \(n\) 个结点的树,你从点 \(x\) 出发,每次等概率随机选择一条与所在点相邻的边走过去。有 \(Q\) 次询问,每次询问给定一个集合 \(S\),求如果从 \(x\) 出发一直随机游走,直到点集 \(S\) 中所有点都至少经过一次的话,期望游走几步,\(1 \le n \le 18,1 \le Q \le 5000\)

解题思路

同上一题,对于每个 \(S\) ,考虑每个点期望经过次数。
直接高斯消元肯定不行,考虑找规律。
我们可以将 \(f_x\) 表示为 \(k_x \times f_{fa_x}\) ,同时 \(f_x=\sum_{(x,u)} \frac{f_u}{degree_u}\)
将求和拆开,设儿子的 \(k_x\) 已经求出,那么可将 \(f_x\) 表示为 \(\sum_{u \in son_x} \frac{k_u \times f_x}{d_u} + \frac{f_{fa_x}}{d_{fa_x}}\)
移项可求出 \(k_x\)\(f_1 = \frac{1}{1-\sum_{u \in son_x} \frac{k_u}{d_u}}\) ,顺推出 \(f_i\) 即可。
考虑如何求出经过 \(S\) 中所有点都经过一遍的期望。
经典 \(min-max\) 容斥,\(max(S)\) 表示 \(S\) 中最晚经过点,\(E(max(S))\) 表示经过最晚点的期望,即所有点都经过一遍的期望,\(min(S)\) 表示 \(S\) 中最晚经过点,\(E(min(S))\) 表示第一次遍历到 \(S\) 中点的期望。
那么就转化为对于一棵树,走到某些节点就停止,走的步数的期望的问题,用上面方法就可以解决。
但是预处理时间复杂度是 \(O(2^nn+3^n)\) 的,会超,要加 \(SOSdp\) ,优化到 \(O(2^nn)\)

Code

#include<bits/stdc++.h>
using namespace std;
const long long mod=998244353;
int n,m,start,d[25],d1[10000005];
long long t[1000005],k[25],f[25],f1[1000005],l[10000005];
bool v[25];
vector<int> a[25];
long long pows(long long x,long long y)
{
	long long h=1;
	while(y)
	{
		if(y&1)h=(h*x)%mod;
		x=(x*x)%mod,y>>=1;
	}
	return h;
}
void dfs1(int x,int y)
{
	if(v[x]){k[x]=pows(l[d[y]],mod-2);return;}
	long long q=1,w;
	for(int i=0;i<a[x].size();i++)
	{
		if(a[x][i]==y)continue;
		dfs1(a[x][i],x);
		if(!v[a[x][i]])q=(q-k[a[x][i]]*l[d[a[x][i]]]%mod+mod)%mod;
	}
	w=pows(q,mod-2);
	k[x]=l[d[y]]*w%mod;
	if(x==start)f[x]=w;
	return;
}
void dfs2(int x,int y)
{
	if(v[x])return;
	if(x!=start)f[x]=k[x]*f[y]%mod;
	for(int i=0;i<a[x].size();i++)if(a[x][i]!=y)dfs2(a[x][i],x);
	return;
}
int main()
{
	int x,y,p,q;
	scanf("%d%d%d",&n,&m,&start),p=(1<<n);
	for(int i=1;i<n;i++)scanf("%d%d",&x,&y),a[x].push_back(y),a[y].push_back(x),d[x]++,d[y]++;
	for(int i=1;i<=n;i++)l[i]=pows(i,mod-2);
	for(int i=0;i<p;i++)
	{
		for(int j=i;j;j>>=1)d1[i]+=(j&1);
		memset(v,false,sizeof(v)),memset(k,0,sizeof(k)),memset(f,0,sizeof(f));
		for(int j=1;j<=n;j++)v[j]=(1<<(j-1))&i;
		if(v[start]){t[i]=0;continue;}
		dfs1(start,0),dfs2(start,0);
		for(int j=1;j<=n;j++)
		{
			for(int u=0;u<a[j].size();u++)t[i]=(t[i]+((((v[j])?0:1)*f[j]*l[d[j]])%mod+(((v[a[j][u]])?0:1)*f[a[j][u]]*l[d[a[j][u]]])%mod)%mod)%mod;
		}
		f1[i]=(((d1[i]&1)?(1):(-1))*t[i]*l[2]%mod+mod)%mod;
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=p-1;j>=0;j--)if(j&(1<<(i-1)))f1[j]=(f1[j]+f1[j^(1<<(i-1))]+mod)%mod;
	}
	for(int i=1;i<=m;i++)
	{
		scanf("%d",&q);y=0;
		for(int j=1;j<=q;j++)scanf("%d",&x),y+=(1<<(x-1));
		printf("%lld\n",(f1[y]+mod)%mod);
	}
  return 0;
}

【CF986C】 AND Graph

题目描述

给定一个 \(m\) 个整数的集合,每个整数在 \(0\)\(2^n-1\) 之间,以每一个整数作为顶点建无向图,当两个点 \(x\)\(y\) 做与运算值为 \(0\) 时,则认为 \(x\)\(y\) 是连通的,即 \(x\)\(y\) 之间有一条无向边。请求出图中连通块的个数, \(1 \le n \le 22\)

解题思路

首先 \(a\)\(b = 0\) 等价于 \(a\) 的补集包含 \(b\) ,我们每次只需要深搜即可。
爆搜会超时,我们可以只向少一个数字 \(1\) 的集合连边,同时加记忆化。
但是这样做会漏点,所以我们要开双层图,一层表示原集合,另一层用来搜索。
时间复杂度 \(O(2^n)\)

Code

#include<bits/stdc++.h>
using namespace std;
bool v[8400005],v1[4200005];
int n,m,a[4200005],s,p;
void dfs(int x)
{
	if(v[x])return;
	v[x]=true;
	if(x<p)dfs((x^(p-1))+p);
	else
	{
		if(v1[x-p]&&(!v[x-p]))dfs(x-p);
		for(int i=n-1;i>=0;i--)if(x&(1<<i))dfs(x^(1<<i));
	}
	return;
}
int main()
{
	scanf("%d%d",&n,&m),p=1<<n;
	for(int i=1;i<=m;i++)scanf("%d",&a[i]),v1[a[i]]=1;
	for(int i=1;i<=m;i++)if(!v[a[i]])s++,dfs(a[i]);
	cout<<s;
	
  return 0;
}

动态规划

【CF1442D】 Sum

题目描述

给定 \(n\) 个不降的数组。有一个值 \(ans\),初始为 \(0\)。你需要进行如下操作 \(k\) 次:

  • 选择一个数组,把 \(ans\) 加上数组的第一个元素,之后把它删除。

请求出 \(ans\) 最大是多少,所有数组的元素总个数 \(\leq 10^6\)\(n,k\leq 3000\)

解题思路

考虑动态规划,设 \(f_i\) 表示进行了 \(i\) 次操作所得的最大值。
因为操作顺序不影响答案,一个一个数组考虑。
对于一个长度为 \(l\) 的数组 \(a\),有 \(f_i=max_{j=max(1,i-l+1)}^{i} f_{j-1}+\sum_{u=j}^{i} a_u\) ,不存在决策单调性,难以优化。
模拟一下数据,可以发现,最优解至多只有 \(1\) 个数组没有选完,证明反证即可。
我们考虑枚举没选完的数组,其他每个数组看成一样物品,做其他数组的背包,再对原数组的每个物品做一次背包,时间复杂度 \(O(n^3)\)
再次观察,我们发现枚举没选完的数组每次重新做背包是很慢的,每个数组都会加入背包中很多次,考虑分治。
每次分治将区间分为两半,每次将左/右区间的数组加入背包中,然后继续求右/左区间的最优解,\(l=r\) 时停止分治。
这样做每个数组加入背包中 \(logn\) 次,时间复杂度 \(O(n^2logn)\)

Code

#include<bits/stdc++.h>
#define max(a,b) (a>b?a:b)
using namespace std;
int n,m;
long long s=0,f[3505],b[35][3005],t[3505];
vector<long long> a[3105];
void dijah(int p,int l,int r)
{
	if(l==r)
	{
		s=max(s,f[m]);
		long long q=m-t[l];
		if(q<0)q=0;
		for(int i=q;i<m;i++)s=max(s,f[i]+a[l][m-i-1]);
		return;
	}
	for(int i=1;i<=m;i++)b[p][i]=f[i];
	int mid=(l+r)>>1;
	for(int i=mid+1;i<=r;i++)
	{
		for(int j=m;j>=a[i].size();j--)f[j]=max(f[j],f[j-a[i].size()]+a[i][a[i].size()-1]);
	}
	dijah(p+1,l,mid);
	for(int i=1;i<=m;i++)f[i]=b[p][i];
	for(int i=l;i<=mid;i++)
	{
		for(int j=m;j>=a[i].size();j--)f[j]=max(f[j],f[j-a[i].size()]+a[i][a[i].size()-1]);
	}
	dijah(p+1,mid+1,r);
	for(int i=1;i<=m;i++)f[i]=b[p][i];
	return;
}
int main()
{
	long long p,x,y;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
	{
		y=0,scanf("%lld",&t[i]);
		for(int j=1;j<=t[i];j++)scanf("%lld",&x),y+=x,a[i].push_back(y); 
	}
	dijah(1,1,n);
	cout<<s;

  return 0;
}

【CF633F】The Chocolate Spree

题目描述

给出一棵 \(n\) 个点的树,每个节点有一个权值,求出不相交的两条链的最大权值和,\(1 \le n \le 10^5\)

解题思路

树上 \(dp\)
每个节点存储该节点往下走最长距离、儿子子树中一条链的最长长度、以该节点为一端的向下的链+子树中某一条链的最长长度、子树中一条链、两条链的最长长度。
每次遍历一个新的儿子节点时依次维护即可,时间复杂度 \(O(n)\)

Code

#include<bits/stdc++.h>
using namespace std;
int n;
long long v[100005],f[100005][2],d1[100005],d2[100005],f1[100005];
vector<int> a[100005];
void dfs1(int x,int y)
{
	d1[x]=f[x][0]=v[x];
	for(int i=0;i<a[x].size();i++)
	{
		if(a[x][i]==y)continue;
		dfs1(a[x][i],x);
		f[x][1]=max(f[x][1],f1[x]+d1[a[x][i]]),f[x][1]=max(f[x][1],f[x][0]+f[a[x][i]][0]),f[x][1]=max(f[x][1],f1[a[x][i]]+d1[x]),f[x][1]=max(f[x][1],f[a[x][i]][1]); //双链 
		f1[x]=max(f1[x],d2[x]+v[x]+d1[a[x][i]]),f1[x]=max(f1[x],f1[a[x][i]]+v[x]),f1[x]=max(f1[x],d1[x]+f[a[x][i]][0]);//单链+单链
		f[x][0]=max(f[x][0],d1[x]+d1[a[x][i]]),f[x][0]=max(f[x][0],f[a[x][i]][0]); //单链
		d1[x]=max(d1[x],d1[a[x][i]]+v[x]);//单链 
		d2[x]=max(d2[x],f[a[x][i]][0]);//孩子最长单链 
	}
	return;
}
int main()
{
	int x,y;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%lld",&v[i]);
	for(int i=1;i<n;i++)scanf("%d%d",&x,&y),a[x].push_back(y),a[y].push_back(x);
	dfs1(1,0);
	cout<<f[1][1];
  return 0;
}

【AT_arc101_c】 Ribbons on Tree

题目描述

给定一个大小为 \(n\) 的树,保证 \(n\) 为偶数且小于 \(5000\),您需要给树上的点两两配对,对于一组对子 \((u,v)\),在树上将 \(u\to v\) 的路径染色,定义一个配对方案合法当且仅当所有边都有颜色,求方案数对 \(10^9+7\) 取模。

解题思路

很明显是容斥。
考虑每条边选或不选容斥,时间复杂度 \(O(2^nn)\)
树上 \(dp\) ,设 \(f_{i,j}\) 表示第 \(i\) 号节点的子树内有 \(j\) 个节点没被配对,每次枚举儿子时做一个树上背包,枚举完一个节点时考虑哪些连通块能够不考虑每条边都被覆盖内部配对的方案数,乘上容斥系数加到 \(f_{i,0}\) 里面。
内部不考虑每条边都被覆盖内部配对的方案数很好算,递推即可。
注意 \(dp\) 时赋初值要把 \(f_{i,0}\) 设为 \(1\) ,这样在合并时才不会算错容斥系数,最后取反即可。

Code

#include<bits/stdc++.h>
using namespace std;
const long long mod=1e9+7;
int n,size2[5005];
long long v[5005],p[5005],f[5005][5005],b[5005][5005];
vector<int> a[5005];
long long dijah(long long x,long long y)
{
	long long h=1;
	while(y)
	{
		if(y&1)h=(h*x)%mod;
		x=(x*x)%mod,y>>=1;
	}
	return h;
}
long long C(long long x,long long y)
{
	return (p[x]*dijah(p[x-y],mod-2)%mod)*dijah(p[y],mod-2)%mod;
}
void dfs1(int x,int y)
{
	f[x][1]=1,size2[x]=1;
	for(int i=0;i<a[x].size();i++)
	{
		if(a[x][i]==y)continue;
		dfs1(a[x][i],x);
		for(int j=0;j<=size2[x]+size2[a[x][i]];j++)b[x][j]=0;
		for(int j=size2[x];j>=0;j--)
		{
			for(int u=0;u<=size2[a[x][i]];u++)b[x][j+u]=(b[x][j+u]+f[x][j]*f[a[x][i]][u]%mod)%mod;
		}
		size2[x]+=size2[a[x][i]];
		for(int j=size2[x];j>=0;j--)f[x][j]=b[x][j];
	}
	for(int i=2;i<=size2[x];i+=2)f[x][0]=(f[x][0]-v[i]*f[x][i]%mod+mod)%mod;
	return;
}
int main()
{
	int x,y;p[0]=1,v[2]=1;
	for(int i=1;i<=5000;i++)p[i]=p[i-1]*i%mod;
	scanf("%d",&n);
	for(int i=1;i<n;i++)scanf("%d%d",&x,&y),a[x].push_back(y),a[y].push_back(x);
	for(int i=2;i<=n;i+=2)v[i]=(C(i,i/2)*p[i/2]%mod)*dijah(dijah(2,i/2),mod-2)%mod;
//	for(int i=4;i<=n;i+=2)v[i]=(2*C(i,2)*v[i-2]%mod)*dijah(i,mod-2)%mod;
	dfs1(1,0);
	cout<<mod-f[1][0];
  return 0;
}

【Luogu P1450】 硬币购物

题目描述

共有 \(4\) 种硬币。面值分别为 \(c_1,c_2,c_3,c_4\),某人去商店买东西,去了 \(n\) 次,对于每次购买,他带了 \(d_i\)\(i\) 种硬币,想购买 \(s\) 的价值的东西。请问每次有多少种付款方法,\(1 \le c_i,d_i,s \le 10^5\)

解题思路

考虑无 \(d_i\) 限制,那就是一个完全背包。
有限制怎么做呢?注意到只有 \(4\) 种物品,我们考虑容斥。
每次枚举那些物品超了限制,将 \(s\) 减去 \(\sum_{i \in S_1} c_i \times (d_{i}+1)\) ,查询背包即可。
时间复杂度 \(O(s+n)\)

Code

#include<bits/stdc++.h>
using namespace std;
int c[5],n,d[5],m,m1,q;
long long f[100005],s;
int main()
{
	scanf("%d%d%d%d%d",&c[1],&c[2],&c[3],&c[4],&n);
	f[0]=1;
	for(int i=1;i<=4;i++)
	{
		for(int j=c[i];j<=100000;j++)f[j]+=f[j-c[i]];
	}
	for(int i=1;i<=n;i++)
	{
		scanf("%d%d%d%d%d",&d[1],&d[2],&d[3],&d[4],&m);
		s=0;
		for(int j=0;j<=15;j++)
		{
			m1=m,q=0;
			for(int u=1;u<=4;u++)if(j&(1<<(u-1)))m1-=(d[u]+1)*c[u],q^=1;
			if(m1>=0)s+=q?-f[m1]:(f[m1]);
		}
		printf("%lld\n",s);
	}
  return 0;
}

【YBOJ】 不等关系 弱化版

题目描述

给定一个长度为 \(n-1\) 的字符串 \(S\) ,保证 \(S\) 只包含 \(<,>\)
问有多少个长度为 \(n\) 的排列,满足:对于 \(1 \le i \le n-1\) ,当且仅当 \(s_i\)\(<\)\(p_i<p_{i+1}\)\(1 \le n \le 2000\)

解题思路

跟上一题一样,容斥+\(dp\)
我们考虑全部都为 \(<\) ,再去减去不对的情况。
每次只需要把原来为 \(>\) 的位置不考虑即可。
同时,一段的 \(<\) 其实是钦定了顺序,只需除去 \(l!\) 即可。
\(f_{i,0/1}\) ,表示做到第 \(i\) 位取的段是一段 \(<\) 还是一段不考虑的区间,初始值设为 \(n!\) ,每次除去 \(l!\) 即可。
时间复杂度 \(O(n^2)\)

Code

#include<bits/stdc++.h>
using namespace std;
const long long mod=998244353;
long long pows(long long x,long long y)
{
	long long h=1;
	while(y)
	{
		if(y&1)h=(h*x)%mod;
		x=(x*x)%mod,y>>=1;
	}
	return h;
}
string a;
long long d1[2005],f[2005][2],inv[2005],p[2005],d2[2005];
int main()
{
	cin>>a;a=' '+a,p[0]=1;
	for(int i=1;i<a.size();i++)d1[i]=d1[i-1]+(a[i]=='<'),d2[i]=d2[i-1]+(a[i]=='>');
	for(int i=1;i<=a.size();i++)p[i]=(p[i-1]*i)%mod,inv[i]=pows(p[i],mod-2);
	f[0][0]=1;
	for(int i=1;i<a.size();i++)
	{
		if(a[i]=='>')f[i][0]=1;
		else break;
	}
	if(a[a.size()-1]=='>'&&f[a.size()-1][0]==1)f[a.size()][0]=1;
	for(int i=1;i<=a.size();i++)
	{
		for(int j=1;j<=i;j++)if(d1[j-1]==d1[i-1])f[i][0]=(f[i][0]+f[j-1][1])%mod;
		if(i==a.size()||a[i]=='>')for(int j=1;j<i;j++)if(j==1||a[j-1]=='>')f[i][1]=(f[i][1]+(f[j-1][0]+f[j-1][1])*(((d2[i-1]-d2[j-1])&1)?(-1):1)*inv[i-j+1]+mod)%mod;
	}
	cout<<((f[a.size()][0]+f[a.size()][1]+2*mod)%mod)*p[a.size()]%mod<<'\n';
  return 0;
}

【Luogu P3175】 按位或

题目描述

刚开始你有一个数字 \(0\),每一秒钟你会随机选择一个 \([0,2^n-1]\) 的数字,与你手上的数字进行或(C++,C 的 |,pascal 的 or)操作。选择数字 \(i\) 的概率是 \(p_i\)。保证 \(0\leq p_i \leq 1\)\(\sum p_i=1\) 。问期望多少秒后,你手上的数字变成 \(2^n-1\)\(1 \le n \le 20\)

解题思路

\(min-max\) 容斥。
设对于一个集合的 \(max\) 指的是最晚数字的被或上的时间,\(min\) 指的是最早的时间被或上的时间。
我们可以算出对于一个数 \(x\) ,它每一次操作或上的数是它的子集的概率。
那么我们就可以推出该数里面的某一位最小被异或次数的期望。
然后就是 \(min-max\) 容斥模板了,用 \(SOSdp\)\(O(3^n)\) 优化到 \(O(2^nn)\) 即可。

Code

#include<bits/stdc++.h>
using namespace std;
int n,p,d[2000005];
double v[2000005],f[2000005],s;
int main()
{
	scanf("%d",&n),p=(1<<n);
	for(int i=0;i<p;i++)cin>>v[i],f[i]=v[i];
	for(int i=0;i<n;i++)d[1<<i]=1;
	for(int i=1;i<=n;i++)
	{
		for(int j=p-1;j>=0;j--)if(j&(1<<(i-1)))f[j]+=f[j^(1<<(i-1))],d[j]+=d[j^(1<<(i-1))];
	}
	for(int i=1;i<p;i++)
	{
		if(f[(p-1)^i]==1)
		{
			cout<<"INF";
			return 0;
		}
		if(d[i]&1)s+=1.00000/(1-f[(p-1)^i]);
		else s-=1.00000/(1-f[(p-1)^i]);
	}
	printf("%.9f",s);
  return 0;
}

【YBOJ】 删数字

题目描述

给出 \(n\) 个数 \(a_i\) ,每次有 \(\frac{a_x}{\sum_{i \in S}a_i}\) 删掉第 \(x\) 个数,\(S\) 为未删掉的数的集合。
再给出 \(n-1\) 个数 \(p_i\) ,要求第 \(i\) 个数必须早于第 \(p_{i+1}\) 个数删除,问删除顺序满足题目要求的概率为多少,保证 \(p_{i+1} < i, 1\le n \le 5000, 1\le \sum_{i=1}^{n}a_i \le 5000\)

解题思路

考虑容斥+ \(dp\)
对于一个 \(x\) 要求早于 \(y\) 删除,我们在图上表示为 \(x\)\(y\) 连一条边,那么最终会表示成一棵内向树。
由于需要考虑到未删除数的集合,正着不好想,考虑容斥。
容斥套路就是考虑相对于原情况反着的与不考虑的,所以我们考虑原树的边反过来,即是一棵外向树。
外向树满足要求的概率很好考虑,首先要把根删掉,然后又拆成若干外向树,直接把外向树森林的满足要求的概率相乘即可。
而对于一条边不考虑的情况,就是断掉这条边,和上面一样,变成若干棵外向树。
因为答案与未删掉的数的集合的大小有关,设 \(f_{i,j}\) 表示以第 \(i\) 个节点为根,所有数的和为 \(j\) ,满足要求删完的概率,在树上做一个像树上背包一样的东西,转移时维护容斥系数。
时间复杂度 \(O(n^2)\)

Code

#include<bits/stdc++.h>
using namespace std;
const long long mod=998244353;
int n;
long long v[5005],f[5005][5005],b[5005],size1[5005],d[5005],v1[5005],p[5005];
vector<int> a[5005];
long long pows(long long x,long long y)
{
	long long h=1;
	while(y)
	{
		if(y&1)h=(h*x)%mod;
		x=(x*x)%mod,y>>=1;
	}
	return h;
}
void dfs1(int x,int y)
{
	size1[x]=v[x],f[x][v[x]]=-1;
	for(int i=0;i<a[x].size();i++)
	{
		if(a[x][i]==y)continue;
		dfs1(a[x][i],x);
		for(int j=size1[x];j>=0;j--)
		{
			for(int u=size1[a[x][i]];u>=0;u--)b[j+u]=(b[j+u]-(f[x][j]*f[a[x][i]][u]%mod)*((long long)(j)*p[u+j]%mod)%mod+mod)%mod;
		}
		size1[x]+=size1[a[x][i]];
		for(int j=0;j<=size1[x];j++)f[x][j]=(f[x][j]*v1[a[x][i]]%mod+b[j])%mod,b[j]=0;
	}
	for(int j=1;j<=size1[x];j++)v1[x]=(f[x][j]+v1[x])%mod;
	return;
}
int main()
{
	long long s=0;
	int x;
	scanf("%d",&n);
	for(int i=1;i<=5000;i++)p[i]=pows(i,mod-2);
	for(int i=1;i<=n;i++)scanf("%lld",&v[i]);
	for(int i=2;i<=n;i++)scanf("%d",&x),a[x].push_back(i);
	dfs1(1,0);
	for(int i=1;i<=size1[1];i++)s=(s+f[1][i]%mod)%mod;
	if(n&1)cout<<(-s+mod)%mod<<'\n';
	else cout<<(s+mod)%mod;


  return 0;
}

【YBOJ】 猎人杀 弱化版

题目描述

猎人杀是一款风靡一时的游戏“狼人杀”的民间版本,他的规则是这样的:
一开始有 \(n\) 个猎人,第 \(i\) 个猎人有仇恨度 \(w_i\) ,每个猎人只有一个固定的技能:死亡后必须开一枪,且被射中的人也会死亡。
然而向谁开枪也是有讲究的,假设当前还活着的猎人有 \([i_1\ldots i_m]\),那么有 \(\frac{w_{i_k}}{\sum_{j = 1}^{m} w_{i_j}}\) 的概率是向猎人 \(i_k\) 开枪。
一开始第一枪由你打响,目标的选择方法和猎人一样(即有 \(\frac{w_i}{\sum_{j=1}^{n}w_j}\) 的概率射中第 \(i\) 个猎人)。由于开枪导致的连锁反应,所有猎人最终都会死亡,现在 \(1\) 号猎人想知道它是最后一个死的的概率。
\(1 \le \sum_{i=1}^n w_i \le 5000\)

解题思路

转换一下,由于不好维护剩余的人 \(w_i\) 的和,我们改变一下打的规则:每次有 \(\frac{w_i}{ \sum_{i=1}^n w_i}\) 选中第 \(i\) 个人,若没死就打他,这样和原来是一样的。
直接求不好求,考虑容斥,设在 \(1\) 之后被打死的集合为 \(S\) ,那么只要让除了 \(1\)\(S\) 之外的全死掉在打死 \(1\) 即可。
\(dp\) 即可,时间复杂度 \(O(n^2)\)

Code

#include<bits/stdc++.h>
using namespace std;
const long long mod=998244353;
long long pows(long long x,long long y)
{
	long long h=1;
	while(y)
	{
		if(y&1)h=(h*x)%mod;
		x=(x*x)%mod,y>>=1;
	} 
	return h;
}
int n;
long long a[5005],m,f[5005];
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
	f[a[1]]=1,m=a[1];
	for(int i=2;i<=n;i++)
	{
		m+=a[i];
		for(int j=m;j>=a[i];j--)f[j]=(f[j]-f[j-a[i]]*((j-a[i])*pows(j,mod-2)%mod)%mod+mod)%mod;
	}
	long long s=0;
	for(int i=a[1];i<=m;i++)s=(s+f[i])%mod;
	cout<<s;

  return 0;
}

容斥

【AT_abc266_g】 Yet Another RGB Sequence

题目描述

求符合要求的字符串个数,对 \(998244353\) 取余。
满足要求的字符串 \(s\) 具备以下特性:

  1. \(s\)rgb 构成。
  2. \(s\) 中有 \(R\)r\(G\)g\(B\)b\(k\)rg

\(1 \le R,G,B,k \le 10^6\)

解题思路

考虑将 \(rg\) 看成一个字符,那就是一个组合问题,答案为 \(\frac{(R+G+B-k)!}{(R-k)! \times (G-k)! \times B! \times k!}\)
但是只能有 \(k\) 个,这样做会有重复,所以就要套二项式反演,枚举 \(rg\) 的个数。
套二项式反演公式,时间复杂度 \(O(nlogn)\)

Code

#include<bits/stdc++.h>
using namespace std;
const long long mod=998244353;
long long a,b,c,d,p[4000005],s=0;
long long pows(long long x,long long y)
{
	if(x==0)return 1;
	long long h=1;
	while(y)
	{
		if(y&1)h=(h*x)%mod;
		x=(x*x)%mod,y>>=1;
	}
	return h;
}
long long C(long long x,long long y)
{
	return (p[x]*pows(p[x-y],mod-2)%mod)*pows(p[y],mod-2)%mod;
}
int main()
{
	scanf("%lld%lld%lld%lld",&a,&b,&c,&d);p[0]=1;
	for(int i=1;i<=4000000;i++)p[i]=(p[i-1]*i)%mod;
	for(int i=d;i<=min(a,b);i++)s=(s+mod+(((i-d)&1)?(1):(-1))*(((p[a+b+c-i]*C(i,d)%mod)*pows(p[a-i],mod-2)%mod)*pows(p[i],mod-2)%mod)*(pows(p[b-i],mod-2)*pows(p[c],mod-2)%mod)%mod)%mod;
	cout<<(mod-s)%mod;
  return 0;
}
posted @ 2024-04-26 09:36  dijah  阅读(25)  评论(0编辑  收藏  举报