#25 CF1172E & CF1299D & CF1097E

Nauuo and ODT

题目描述

点此看题

解法

直接贡献法,对每种颜色分别统计贡献。假设计算到颜色 \(c\) 的时候,我们把是颜色 \(c\) 的点染白,把不是颜色 \(c\) 的点染黑。考虑正难则反,不包含这种颜色的路径数就是 \(\sum\) 黑色连通块大小的平方。

维护这东西其实是经典问题,我们把每个点的颜色放在其父边上,黑色代表连接,白色代表断开。这样转化之后,我们把图上的每个连通块去掉根节点,那么剩下的连通块就是黑色连通块了。

直接上 \(\tt lct\),维护虚子树的大小和虚子树大小的平方,把每个联通块的根贡献进答案(\(access\) 之后虚子树的平方和)

修改考虑离线处理,就可以转化成 \(n+2m\) 个关于某个点在某时刻变色的信息,可以差分贡献回去。

时间复杂度 \(O(n\log n)\)

#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
const int M = 400005;
#define ll long long
#define pb push_back
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,a[M],b[M],fa[M],ch[M][2],p[M];
ll s[M],v1[M],v2[M],c[M],ans;
struct node{int x,y;};
vector<int> g[M];vector<node> v[M];
void dfs(int u)
{
	for(int v:g[u]) if(v^p[u])
		p[v]=u,dfs(v);
}
void up(int x)
{
	s[x]=s[ch[x][0]]+s[ch[x][1]]+v1[x]+1;
}
int nrt(int x)
{
	return x==ch[fa[x]][0] || x==ch[fa[x]][1];
}
int chk(int x)
{
	return x==ch[fa[x]][1];
}
void rotate(int x)
{
	int y=fa[x],z=fa[y],k=chk(x),w=ch[x][k^1];
	ch[y][k]=w;fa[w]=y;
	if(nrt(y)) ch[z][chk(y)]=x;fa[x]=z;
	ch[x][k^1]=y;fa[y]=x;
	up(y);up(x);
}
void splay(int x)
{
	while(nrt(x))
	{
		int y=fa[x];
		if(nrt(y))
		{
			if(chk(x)==chk(y)) rotate(y);
			else rotate(x);
		}
		rotate(x);
	}
}
void access(int x)
{
	for(int y=0;x;x=fa[y=x])
	{
		splay(x);
		v1[x]+=s[ch[x][1]];
		v2[x]+=s[ch[x][1]]*s[ch[x][1]];
		ch[x][1]=y;
		v1[x]-=s[ch[x][1]];
		v2[x]-=s[ch[x][1]]*s[ch[x][1]];
		up(x);
	}
}
void make(int u)
{
	access(u);splay(u);
}
int findrt(int u)
{
	make(u);
	while(ch[u][0]) u=ch[u][0];
	splay(u);return u;
}
void add(int u,int f)
{
	int rt=findrt(u);make(rt);
	ans+=f*v2[rt];
}
void link(int u,int v)
{
	add(u,-1);add(v,-1);make(u);make(v);
	fa[u]=v;v1[v]+=s[u];v2[v]+=s[u]*s[u];
	add(v,1);
}
void cut(int u,int v)
{
	add(u,-1);make(u);
	fa[ch[u][0]]=0;ch[u][0]=0;
	up(u);add(u,1);add(v,1);
}
signed main()
{
	n=read();m=read();
	for(int i=1;i<=n;i++)
		v[a[i]=read()].pb({i,0});
	for(int i=1;i<n;i++)
	{
		int u=read(),v=read();
		g[u].pb(v);g[v].pb(u);
	}
	dfs(1);p[1]=n+1;
	for(int i=1;i<=n+1;i++) s[i]=1;
	for(int i=1;i<=n;i++) link(i,p[i]);
	for(int i=1;i<=m;i++)
	{
		int x=read(),y=read();
		v[a[x]].pb({x,i});
		v[a[x]=y].pb({x,i});
	}
	for(int i=1;i<=n;i++)
	{
		ll last=0;int x=0;
		for(auto t:v[i])
		{
			b[x=t.x]?link(x,p[x]):cut(x,p[x]);
			c[t.y]+=1ll*n*n-ans-last;
			last=1ll*n*n-ans;b[x]^=1;
		}
		for(auto t:v[i]) if(b[x=t.x])
			link(x,p[x]),b[x]=0;
	}
	for(int i=1;i<=m;i++)
		c[i]+=c[i-1];
	for(int i=0;i<=m;i++)
		printf("%lld\n",c[i]);
}

Around the World

题目描述

点此看题

解法

关键的 \(\tt observation\) 是:对路径权值有影响的只有环的异或和,那么存在合法路径权值为 \(0\) 等价于存在选取若干个环的方案,使得这些环可以到达并且环的异或和为 \(0\)

那么有一个显然的思路:我们把可以到达环的权值的线性基作为状态 \(dp\),分别扫描每个连通块,转移就讨论合并上这个连通块的线性基,或者不合并。显然本题的难点是代码实现,下面按照时间顺序梳理思路。

首先我们处理出所有可能的线性基,可以直接搜索所有的最简线性基,发现只有 \(k=374\) 种。我们存下这些线性基,并且把它们哈希方便 \(dp\) 的时候获取编号。

处理每个连通块的时候,我们需要把连通块里面所有环插入线性基中。考虑这个连通块的 \(\tt dfs\) 树,发现我们只需要插入返祖边构成的环即可,注意插入线性基时还需要维护其最简性,此外还需记录是否可以异或和为 \(0\),也就是新插入的数可以被线性基里面已有的元素表示出来。

利用题目条件 不存在一个长度 >3 的简单环经过了 1 号点,说明 \(1\) 最多向这个连通块连两条边。对于两条边的情况,我们还需要考虑只断一条边的转移,断一条边仅仅只会减少和根相连的这个环。所以在 \(\tt dfs\) 的时候还需要处理另一种线性基,这种线性基不插入和根相连的环。

时间复杂度 \(O(nk)\)

#include <cstdio>
#include <vector>
#include <cstring>
#include <iostream>
using namespace std;
const int M = 100005;
const int MOD = 1e9+7;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,k,zxy,Ind,id[M],dfn[M],d[M],dp[M],f[M];
struct edge{int v,c;};vector<edge> g[M];
struct node
{
	int a[5],fl;
	node() {fl=0;memset(a,0,sizeof a);}
	void clear() {fl=0;memset(a,0,sizeof a);}
	int H()
	{
		return a[0]+a[1]*2+a[2]*8+
		a[3]*64+a[4]*1024;
	}
	void ins(int x)
	{
		for(int i=4;i>=0;i--) if(x>>i&1)
		{
			if(!a[i])
			{
				for(int j=i-1;j>=0;j--)
					if(x>>j&1) x^=a[j];
				for(int j=4;j>i;j--)
					if(a[j]>>i&1) a[j]^=x;
				a[i]=x;return;
			}
			else x^=a[i];
		}
		fl=1;
	}
}lb[400],A,B;
void add(int &x,int y) {x=(x+y)%MOD;}
void get(int x,int s)
{
	if(x==-1)
	{
		lb[k]=A;id[A.H()]=k;k++;
		return ;
	}
	A.a[x]=0;get(x-1,s);
	if(!(s>>x&1)) for(int i=0;i<(1<<x);i++)
		A.a[x]=(1<<x)|i,get(x-1,s|i);
}
int merge(node a,node b)
{
	node r=a;
	for(int i=4;i>=0;i--)
		if(b.a[i]) r.ins(b.a[i]);
	return r.fl?k:id[r.H()];
}
void dfs(int u,int fa)
{
	dfn[u]=++Ind;
	for(auto x:g[u])
	{
		int v=x.v,c=x.c;
		if(v==fa) continue;
		if(!dfn[v]) d[v]=d[u]^c,dfs(v,u);
		else if(dfn[u]>dfn[v])
		{
			A.ins(d[u]^d[v]^c);
			if(v!=1) B.ins(d[u]^d[v]^c);
			else zxy=1;
		}
	}
}
int main()
{
	n=read();m=read();get(4,0);
	for(int i=1;i<=m;i++)
	{
		int u=read(),v=read(),c=read();
		g[u].push_back({v,c});
		g[v].push_back({u,c});
	}
	dp[0]=dfn[1]=Ind=1;
	for(auto x:g[1])
	{
		int v=x.v,c=x.c;
		if(dfn[v]) continue;
		zxy=0;A.clear();B.clear();
		d[v]=c;dfs(v,1);
		for(int i=0;i<k;i++) f[i]=dp[i];
		//do not delete
		if(!A.fl) for(int i=0;i<k;i++)
			if(f[i]) add(dp[merge(lb[i],A)],f[i]);
		//delete one of them (if zxy is true)
		if(zxy && !B.fl) for(int i=0;i<k;i++)
			if(f[i]) add(dp[merge(lb[i],B)],2*f[i]%MOD);
	}
	int ans=0;
	for(int i=0;i<k;i++) add(ans,dp[i]);
	printf("%d\n",ans);
}

Egor and an RPG game

题目描述

点此看题

解法

其实我们并不需要知道 \(f(n)\) 是什么,只需要给他定一个较紧的下界。考虑构造这样一种分层下降的排列,第 \(i\) 层有 \(i\) 个元素,即 \(\{1,3,2,6,5,4,10,9,8,7....\}\),那么如果有 \(k\) 个满层,则需要至少 \(k\) 个划分:

\[f(n)\geq\max\{\ k\ |\ \frac{k(k+1)}{2}\leq n\ \}=c(n) \]

那么我们尝试构造一种只需要 \(c(n)\) 个的划分,可以归纳地构造,首先求出当前序列的 \(\tt LIS\),设长度为 \(m\)

\(m>c(n)\),可以直接把这个 \(\tt LIS\) 划分出来,已知长度为 \(n-m\) 只需要 \(c(n-m)\) 步就可以构造出来。而我们又知道 \(c(n-m)\leq c(n)-1\),自然归纳到了更小的情况。

\(m\leq c(n)\),根据 \(\tt dilworth\) 定理,原序列一定可以划分成 \(m\) 个下降子序列(并且不能再少了),这种情况就是归纳的出口。具体构造可以考虑求 \(\tt LIS\) 的经典方法,直接把它放在 \(\tt lower\_bound\) 到的位置的下降子序列中即可。

由于递归次数是 \(O(\sqrt n)\) 的,并且每一次递归都需要暴力求 \(\tt LIS\),时间复杂度 \(O(n\sqrt n\log n)\)

总结

对于这种 构造次数不超过所有情况中最劣的最小次数 的题目,可以先确定一个较紧的下界,然后归纳地构造。

#include <cstdio>
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 100005;
#define vi vector<int>
#define pb push_back
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int T,n,m,b[M],q[M],p[M],v[M];vi ans[M];
int get(int n)
{
	int r=0;
	while(r*(r+1)/2<=n) r++;
	return r-1;
}
void work(vi a)
{
	int n=a.size()-1,k=0;
	if(n<=0) return ;
	for(int i=1;i<=n;i++)
	{
		int x=lower_bound(q+1,q+1+k,i,
		[&](int i,int j){return a[i]<a[j];})-q;
		p[i]=q[x-1];q[x]=i;b[i]=x;k+=(x==k+1);
	}
	if(k>get(n))
	{
		for(int i=0;i<=n;i++) b[i]=0;
		for(int i=q[k];i;i=p[i]) b[i]=1;
		m++;vi d;d.pb(0);
		for(int i=1;i<=n;i++)
		{
			if(b[i]) ans[m].pb(a[i]);
			else d.pb(a[i]);
		}
		work(d);return ;
	}
	for(int i=1;i<=n;i++)
		ans[m+b[i]].pb(a[i]);
	m+=k;
}
int main()
{
	T=read();
	while(T--)
	{
		n=read();m=0;vi a;a.pb(0);
		for(int i=1;i<=n;i++) a.pb(read());
		work(a);
		printf("%d\n",m);
		for(int i=1;i<=m;i++)
		{
			printf("%d",ans[i].size());
			for(int x:ans[i]) printf(" %d",x);
			ans[i].clear();puts("");
		}
	}
}
posted @ 2022-06-05 16:43  C202044zxy  阅读(152)  评论(0编辑  收藏  举报