@1 UOJ91 & UOJ418 & UOJ94

[集训队互测2015] 最大异或和

题目描述

点此看题

解法

只要维护线性基就可以很好地回答询问,有一个关键的 \(\tt observation\) 是:维护原序列的线性基等价于维护差分序列的线性基。这样操作一反映到线性基上变成了两个单点修改,操作二变成了两个单点修改加上区间清零,用势能法即可说明总修改次数是 \(O(n)\) 的。

这题肯定要用 bitset 优化,那么问题变成了如何在 \(O(\frac{n^2}{w})\) 删除和插入一个元素。

其实写个带删除线性基就行了,但是我以前没有听说过这东西,所以简单讲解一下它的思路。对于每个数我们需要额外维护它被哪些数字所表示(也就是在线性基构建过程中异或上了那些数),假设现在要删除 \(x\)

  • 如果 \(x\) 不在线性基中,直接删除。
  • 否则找到一个不在线性基中,且可以被 \(x\) 表示的数 \(y\),那么 \(y\) 是可以替换 \(x\) 的,此时线性基的大小不变。那么我们把所有被 \(x\) 表示的数异或上 \(y\) 的被表示集合,就可以达到删除 \(x\) 的效果。
  • 若不存在这样的 \(y\),则线性基的大小必须减少。我们找到最小的可以被 \(x\) 表示的数字 \(z\),那么删除 \(x\) 之后 \(z\) 这个基必定消除,此后由 \(z\) 来顶替 \(x\) 的位置。所以我们把所有被 \(x\) 表示的数异或上 \(z\),由于 \(z\) 最小所以线性基大小恰好减一

时间复杂度 \(O(\frac{n^3}{w})\)\(n,m,q\) 视作同阶),在下面的代码实现中,新建两个 bitset \(c[x],v[x]\) 分别表示数字 \(x\) 插入线性基后的结果,和数字 \(x\) 被哪些数字所表示,上述删除的过程可以简化成十分简洁的代码。

#include <cstdio>
#include <bitset>
#include <iostream>
using namespace std;
const int M = 2005;
#define bs bitset<M>
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,q,p[M];bs w,a[M],c[M],v[M];
void ins(int x)
{
	for(int i=m-1;i>=0;i--) if(c[x][i])
	{
		if(p[i]) c[x]^=c[p[i]],v[x]^=v[p[i]];
		else {p[i]=x;break;}
	}
}
void upd(int x,bs b)// x xor b
{
	if(x>n || b.none()) return ;
	int o=0;
	for(int i=1;i<=n;i++)//find y
		if(c[i].none() && v[i][x]) {o=i;break;}
	if(!o) for(int i=0;i<m;i++)//find z
		if(p[i] && v[p[i]][x]) {o=p[i];p[i]=0;break;}
	for(int i=1;i<=n;i++)// combine nothing / y / z
		if(i!=o && v[i][x]) v[i]^=v[o],c[i]^=c[o];
	c[o]^=b;ins(o);
}
signed main()
{
	ios::sync_with_stdio(0);cin.tie(0);
	cin>>n>>m>>q;
	for(int i=1;i<=n;i++)
		cin>>a[i],c[i]=a[i-1]^a[i],v[i][i]=1,ins(i);
	while(q--)
	{
		int op=0,l=0,r=0;cin>>op;
		if(op==3)
		{
			w.reset();
			for(int i=m-1;i>=0;i--)
			{
				if(!w[i] && p[i]) w^=c[p[i]];
				cout<<w[i];
			}
			cout<<endl;continue;
		}
		cin>>l>>r>>w;
		if(op==1)
		{
			upd(l,w);upd(r+1,w);
			for(int i=l;i<=r;i++) a[i]^=w;
		}
		if(op==2)
		{
			upd(l,a[l]^w);upd(r+1,a[r]^w);
			for(int i=l+1;i<=r;i++) upd(i,a[i]^a[i-1]);
			for(int i=l;i<=r;i++) a[i]=w;
		}
	}
}

[集训队作业2018] 三角形

题目描述

点此看题

解法

超级好题啊!(我还被自己的错误想法困扰了好久😭

首先考虑计算 \(1\) 的答案,一个关键的 \(\tt observation\) 是:当我们放置 \(w_i\) 后,会立刻回收所有儿子的 \(w_v\);根据这个观察,我们可以把问题转化到序列上,每次往序列后加入一个点 \(i\),要求加入 \(i\) 时它的儿子 \(v\) 都已经被加入,然后会发生这样的事情:

  • 现有的石子数 \(sum\) 增加 \(w_i\) 的石子数。
  • 现有的石子数 \(sum\) 减少 \(\sum w_v\) 的石子数。

那么需要的石子数 \(mx\) 就是 \(sum\) 的历史最大值,可以用一个二元组 \((sum,mx)\) 来描述。点 \(i\) 的二元组是 \((w_i-\sum w_v,w_i)\),操作的过程可以描述为二元组的合并,我们这样合并两个二元组 \((a_1,b_1)\)\((a_2,b_2)\)

\[(a_1,b_1)+(a_2,b_2)=(a_1+a_2,\max(b_1,a_1+b_2)) \]

如果不考虑儿子的限制,那么可以用简单的排序贪心解决。即对于两个二元组 \(x,y\),如果 (x+y).mx<(y+x).mx,那么就把 \(x\) 放前面。排序之后的顺序就是操作顺序,并且不难证明不会出现形如 \(A<B,B<C,C<A\) 无法排序的情况。

但是儿子的限制还是难以考虑,我们反转操作序列,把限制转化成父亲的限制。具体来说就是逆序进行原来的操作序列,那么加入一个点 \(i\) 时需要满足它的父亲已经被加入,由于逆序会发生这样的事情:

  • 现有的石子数 \(sum\) 增加 \(\sum w_v\) 的石子数。
  • 现有的石子数 \(sum\) 减少 \(w_i\) 的石子数。

那么点 \(i\) 的二元组就变成了 \((\sum w_v-w_i,\sum w_v)\),受到逆序的影响,合并规则还是不变。

逆序的正确性来自于,逆序的每个操作后缀,对应着正序的每个操作前缀的操作效果。

现在得到了一个经典问题,我们可以找到优先级最高的点 \(x\),如果 \(x\) 的父亲已经被加入,那么直接加入 \(x\);否则在 \(x\) 的父亲被加入时 \(x\)立刻被加入,这构成了一个依赖关系,把 \(x\) 和它的父亲合并即可。

现在解决了 \(1\) 的问题,对于每个子树,由于操作顺序不会改变,所以可以预处理出操作顺序。然后以操作顺序建立一棵线段树,通过线段树合并得到每个子树的线段树,就可以算出答案,时间复杂度 \(O(n\log n)\)

总结

限制对象的切换十分巧妙,本来是各种各样的儿子,并不好考虑限制;但是切换之后变成了单一的父亲,可以套用经典模型。

#include <cstdio>
#include <vector>
#include <iostream>
#include <algorithm>
#include <set>
using namespace std;
const int M = 200005;
#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,rt[M],w[M],p[M],fa[M],vis[M],b[M];
vector<int> g[M],vc[M];ll ans[M],w2[M];
struct node
{
	ll x,y;int id;
	node(ll X=0,ll Y=0,int I=0) : x(X) , y(Y) , id(I) {}
	node operator + (const node &b) const
		{return node(x+b.x,max(y,x+b.y),id);}
	bool operator < (const node &b) const
	{
		ll t1=(*this+b).y,t2=(b+*this).y;
		if(t1!=t2) return t1<t2;
		if(x!=b.x) return x<b.x;
		if(y!=b.y) return y<b.y;
		return id<b.id;
	}
}a[M];set<node> h;
node s[M*30];int cnt,ls[M*30],rs[M*30];
int find(int x)
{
	if(x!=fa[x]) fa[x]=find(fa[x]);
	return fa[x];
}
void dfs(int u)
{
	vis[u]=1;b[u]=++m;
	for(int v:vc[u]) dfs(v);
}
void ins(int &x,int l,int r,int p)
{
	if(!x) x=++cnt;
	if(l==r) {s[x]=a[l];return ;}
	int mid=(l+r)>>1;
	if(mid>=p) ins(ls[x],l,mid,p);
	else ins(rs[x],mid+1,r,p);
	s[x]=s[ls[x]]+s[rs[x]];
}
int merge(int x,int y)
{
	if(!x || !y) return x+y;
	ls[x]=merge(ls[x],ls[y]);
	rs[x]=merge(rs[x],rs[y]);
	s[x]=s[ls[x]]+s[rs[x]];
	return x;
}
void work(int u)
{
	ins(rt[u],1,n,b[u]);
	for(int v:g[u])
		work(v),rt[u]=merge(rt[u],rt[v]);
	ans[u]=s[rt[u]].y;
}
signed main()
{
	read();n=read();
	for(int i=2;i<=n;i++)
		p[i]=read(),g[p[i]].pb(i);
	for(int i=1;i<=n;i++)
		w[i]=read(),w2[p[i]]+=w[i],fa[i]=i;
	for(int i=1;i<=n;i++)
		a[i]=node(w2[i]-w[i],w2[i],i),h.insert(a[i]);
	for(int T=1;T<=n;T++)
	{
		int x=h.begin()->id;
		h.erase(h.begin());
		if(x==1 || vis[p[x]]) dfs(x);
		else
		{
			int u=find(p[x]);
			h.erase(a[u]);
			a[u]=a[u]+a[x];
			h.insert(a[u]);
			vc[u].pb(x);fa[x]=u;
		}
	}
	for(int i=1;i<=n;i++)//get the order
		a[b[i]]=node(w2[i]-w[i],w2[i],i);
	work(1);
	for(int i=1;i<=n;i++)
		printf("%lld ",ans[i]+w[i]);
}

[集训队互测2015] 胡策的统计

题目描述

点此看题

解法

首先讲解一下 \(dp\) 的方法,像我这样生成函数完全不会的选手就指望这个骗点分了

\(f(S)\) 表示考虑集合 \(S\) 的导出子图,其中有多少个生成子图是连通图,转移正难则反,设 \(m(S)\) 表示集合 \(S\) 导出子图的边数,注意我们需要强制一个点在集合 \(T\) 中才能不算重(虽然这是老生常谈了):

\[f(S)=2^{m(S)}-\sum_{T\subset S} [v\in T] f(T)\cdot 2^{m(S-T)} \]

\(g(S)\) 表示集合 \(S\) 的连通值之和,把生成子图和计算连通值混合计数,也就是数出原图的连通块有多少种排列方式,那么枚举排在最前面的连通块即可:

\[g(S)=f(S)+\sum_{T\subset S,T\not=\varnothing} f(T)\cdot g(S-T) \]

那么时间复杂度 \(O(3^n)\),可以获得 \(60\) 分的高分。


导出子图的连通生成子图计数是经典问题,在我这篇古老的 集合幂级数和状压dp 有讲解,但还是再梳理下思路。

(所以下文只是梳理思路,详细讲解可以看我给的那篇博客)

考虑加速求解 \(f(S)\),设 \(h(S)=2^{m(S)}(S\not=\varnothing)\),把它们都看成集合幂级数,可以列出下面的等式:

\[1+h=\sum_{k\geq 0}\frac{f^k}{k!}=e^f \]

对于第一个等号,左边的含义是导出子图的生成子图个数;右边的含义是,用若干个连通块拼出了生成子图,但是由于连通块之间有顺序,所以要除去 \(k!\);继续变形这个式子:

\[\ln(1+h)=f \]

那么问题变成了快速求 \(f=\ln(1+h)\),我们先把集合幂级数通过莫比乌斯正变换转化成集合占位幂级数,然后对于每个集合 \(S\),可以获得一个 \(n\) 次多项式,用形式幂级数的方式对它求 \(\ln\) 即可。

对于形式幂级数求 \(a=\ln(1+b)\) 的问题,求导之后就可以获得递推式,这一部分时间复杂度 \(O(n^22^n)\)

知道 \(f(S)\) 之后求 \(g(S)\) 就是普通的子集卷积,这一部分复杂度也是 \(O(n^22^n)\)

总时间复杂度 \(O(n^22^n)\)

#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int M = (1<<20)+5;
const int MOD = 998244353;
#define ll long long
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,f[22][M],g[22][M],b[M];
int pw[500],inv[500],x[500],y[500];
void fwt(int *a,int n,int op)
{
	for(int i=1;i<n;i<<=1)
		for(int j=0;j<n;j+=i<<1)
			for(int k=0;k<i;k++)
			{
				if(op==1) a[i+j+k]=(a[i+j+k]+a[j+k])%MOD;
				else a[i+j+k]=(a[i+j+k]-a[j+k]+MOD)%MOD;
			}
}
signed main()
{
	n=read();m=read();inv[0]=inv[1]=pw[0]=1;
	for(int i=1;i<=m;i++) pw[i]=2ll*pw[i-1]%MOD;
	for(int i=2;i<=n;i++)
		inv[i]=(ll)inv[MOD%i]*(MOD-MOD/i)%MOD;
	for(int i=1;i<=m;i++)
		x[i]=read()-1,y[i]=read()-1;
	for(int s=0;s<(1<<n);s++)
	{
		int t=0;b[s]=b[s>>1]+(s&1);
		for(int i=1;i<=m;i++)
			t+=((s>>x[i]&1) && (s>>y[i]&1));
		f[b[s]][s]=pw[t];
	}
	for(int i=1;i<=n;i++) fwt(f[i],1<<n,1);
	// get ln
	for(int i=1;i<=n;i++)
	{
		for(int k=0;k<i;k++)
			for(int j=0;j<1<<n;j++)
				g[i][j]=(g[i][j]+(ll)k*g[k][j]%MOD*f[i-k][j])%MOD;
		for(int j=0;j<1<<n;j++)
			g[i][j]=(f[i][j]-(ll)inv[i]*g[i][j]%MOD+MOD)%MOD;
	}
	//
	memset(f,0,sizeof f);
	for(int i=0;i<1<<n;i++) f[0][i]=1;
	for(int i=1;i<=n;i++)
		for(int k=0;k<i;k++)
			for(int j=0;j<1<<n;j++)
				f[i][j]=(f[i][j]+(ll)f[k][j]*g[i-k][j])%MOD;
	fwt(f[n],1<<n,-1);
	printf("%lld\n",f[n][(1<<n)-1]);
}
posted @ 2022-07-05 22:30  C202044zxy  阅读(325)  评论(6编辑  收藏  举报