• @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]);
    }
    
  • 相关阅读:
    推销
    5132. 颜色交替的最短路径
    5130. 等价多米诺骨牌对的数量
    @babel/plugin-transform-runtime和@babel/preset-env的区别
    5128. 最深叶节点的最近公共祖先(二叉树)
    1094. 拼车
    1109. 航班预订统计(数组)
    5129. 表现良好的最长时间段(数组)
    path.resove 和 path.join
    【原生】 call、apply、bind 的基本使用方法,已经解析了某些源码
  • 原文地址:https://www.cnblogs.com/C202044zxy/p/16448895.html
Copyright © 2020-2023  润新知