[做题笔记] 2-sat 问题的进阶应用

对称性

考虑 \(\tt 2sat\) 边的意义是:如果选取了 \(i\) 则必须选取 \(j\),那么如果我们连边 \((i,j)\),我们都是也需要连边 \((inv(j),inv(i))\)\(inv(x)\) 即表示变量 \(x\) 的逆),因为原命题和其逆否命题真假相同。

那么发现这样建出来的图具有某种对称性,此性质是 \(\tt 2sat\) 算法最重要的性质

解的构造

\(col_i\) 表示 \(i\) 所在强连通分量的拓扑序编号(编号小的拓扑序大),那么如果 \(col_i<col_{i+n}\) 我们选取 \(i\);否则我们选取 \(i+n\)

要证明上述构造是正确的,我们只需要证明被选取点不能到达没有未被选取的点。使用反证法,假设存在被选取点 \(x\) 和未被选取 \(y\),并且存在 \(x\rightarrow y\) 的路径,那么显然有 \(col_x\geq col_y\),同时根据对称性,存在一条 \(inv(y)\rightarrow inv(x)\) 的路径,所以有 \(col_{y+n}\geq col_{x+n}\)

根据选取的关系可以知道:\(col_x<col_{x+n},col_{y}>col_{y+n}\),所以可以推出 \(col_{x+n}>col_x\geq col_y>col_{y+n}\),但是这与 \(col_{y+n}>col_{x+n}\) 矛盾,反证法证毕。

特殊的边

这里有一个小 \(\tt trick\):如果 \(i\) 强制不能选取,那么可以连一条 \((i,inv(i))\) 的边,表示强制不能选 \(i\)

这样连边还是满足对称性,用处很多:NOI2017 游戏

字典序最小的解

这种问题建议用直接 \(\tt dfs\) 的方法,也就是先搜字典序小的再搜索字典序大的。

这个问题也是可拓展的,既然是字典序问题就很容易和贪心产生联系:New Language

前后缀优化建图

本质思想还是建虚点来优化建图,连向这个虚点就代表了连向一个前缀\(/\)后缀。

如果你遇到要连向除一个点之外所有点的问题,那么可以拆成前缀与后缀的连边:Duff in Mafia

Ants (树链剖分优化建图)

题目描述

点此看题

解法

我们把路径当成点建 \(\tt 2sat\),每只蚂蚁的两条路径互为逆元。那么如果两条路径 \((x,y)\) 有交,那么可以连接 \((x,inv(y))\)\((y,inv(x))\),有一个建图的小技巧是把 \((x,y)\) 当成无序对,然后把这两条边一次性连好。

我们先把这些路径通过树链剖分放在线段树上,那么我们现在想完成的功能是:把某点和子树中的所有其他点连边。注意这里不要被传统的线段树优化建图束缚住了,这里我们可以魔改经典的前后缀优化建图的思想:

其中黑色点代表原线段树上的节点;红色点代表这一层建出的虚点,点数和节点的路径数量一致;绿色点表示节点的路径;蓝色点表示路径的逆。上述建图方法的主体还是前后缀优化建图,只是为了连到子树内,把上一层的最后一个虚点当成这一层的第一个虚点,时间复杂度 \(O(m\log n^2)\)

#include <cstdio>
#include <vector>
using namespace std;
const int M = 100005;
const int N = 10000005;
#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,cnt,tot,f[M];vector<int> g[N],vc[M<<2];
int Ind,num[M],siz[M],son[M],top[M],fa[M],dep[M];
int k,scc,dfn[N],low[N],col[N],s[N],in[N];
struct edge{int v,next;}e[M<<1];
void add(int u,int v) {g[u].pb(v);g[v^1].pb(u^1);}
void dfs1(int u,int p)
{
	siz[u]=1;fa[u]=p;
	dep[u]=dep[p]+1;
	for(int i=f[u];i;i=e[i].next)
	{
		int v=e[i].v;
		if(v==p) continue;
		dfs1(v,u);
		siz[u]+=siz[v];
		if(siz[v]>siz[son[u]]) son[u]=v;
	}
}
void dfs2(int u,int tp)
{
	top[u]=tp;num[u]=++Ind;
	if(son[u]) dfs2(son[u],tp);
	for(int i=f[u];i;i=e[i].next)
		if(e[i].v^fa[u] && e[i].v^son[u])
			dfs2(e[i].v,e[i].v);
}
void ins(int i,int l,int r,int L,int R,int w)
{
	if(L>r || l>R) return ;
	if(L<=l && r<=R) {vc[i].push_back(w);return ;}
	int mid=(l+r)>>1;
	ins(i<<1,l,mid,L,R,w);
	ins(i<<1|1,mid+1,r,L,R,w);
}
void add(int u,int v,int w)
{
	while(top[u]^top[v])
	{
		if(dep[top[u]]<dep[top[v]]) swap(u,v);
		ins(1,1,n,num[top[u]],num[u],w);
		u=fa[top[u]];
	}
	if(dep[u]<dep[v]) swap(u,v);
	if(u!=v) ins(1,1,n,num[v]+1,num[u],w);
}
void build(int i,int l,int r,int p)
{
	int o=vc[i].size(),u=++cnt,v=(cnt+=o);
	if(o) add(v-1<<1,v<<1);
	else if(p) add(p<<1,v<<1);
	for(int j=0;j<o;j++)
	{
		int w=vc[i][j];add(w,u+j<<1);
		if(j) add(u+j-1<<1,w^1),add(u+j-1<<1,u+j<<1);
		else if(p) add(p<<1,u<<1),add(p<<1,w^1);
	}
	if(l==r) return ;
	int mid=(l+r)>>1;
	build(i<<1,l,mid,v);
	build(i<<1|1,mid+1,r,v);
}
void tarjan(int u)
{
	low[u]=dfn[u]=++Ind;
	s[++k]=u;in[u]=1;
	for(int v:g[u])
	{
		if(!dfn[v])
		{
			tarjan(v);
			low[u]=min(low[u],low[v]);
		}
		else if(in[v])
			low[u]=min(low[u],dfn[v]);
	}
	if(low[u]==dfn[u])
	{
		int v;scc++;
		do
		{
			v=s[k--];
			col[v]=scc;in[v]=0;
		}while(u!=v);
	}
}
signed main()
{
	n=cnt=read();
	for(int i=1;i<n;i++)
	{
		int u=read(),v=read();
		e[++tot]=edge{v,f[u]},f[u]=tot;
		e[++tot]=edge{u,f[v]},f[v]=tot;
	}
	dfs1(1,0);dfs2(1,1);m=read();
	for(int i=1;i<=m;i++)
	{
		add(read(),read(),i<<1);
		add(read(),read(),i<<1|1);
	}
	build(1,1,n,0);Ind=0;
	for(int i=1;i<=2*cnt;i++)
		if(!dfn[i]) tarjan(i);
	for(int i=1;i<=m;i++)
		if(col[i<<1]==col[i<<1|1])
			{puts("NO");return 0;}
	puts("YES");
	for(int i=1;i<=m;i++)
		puts(col[i<<1]<col[i<<1|1]?"1":"2");
}

精准预测(有解结论再探)

题目描述

点此看题

解法

其实这题我完全是有能力做出来的,一定要把遇到的结论总结好,用的时候才可以行云流水。

考虑每个人的每个时刻具有生存和死亡两种状态,那么我们把这个建成 \(\tt 2sat\),记 \(A(x,i)\) 表示第 \(x\) 个人的时刻 \(i\) 是生存状态,\(D(x,i)\) 表示第 \(x\) 个人的时刻 \(i\) 是死亡状态,那么我们这样连边:

  • 难兄难弟:\(D(x,t)\) 连向 \(D(y,t+1)\),同时我们把 \(A(y,t+1)\) 连向 \(A(x,t)\)
  • 死神来了:\(A(x,t)\) 连向 \(D(y,t)\),同时我们把 \(A(y,t)\) 连向 \(D(x,t)\)
  • 因为生存和死亡的连续性,我们把 \(A(x,t)\) 连向 \(A(x,t-1)\),把 \(D(x,t)\) 连向 \(D(x,t+1)\)

连出上面的图还是推荐使用对称性,这样就只用连一半了。

然后我们想要优化这张图的点数,发现只有出现过的 \((x,t)\)\((a,T+1)\) 是需要保留的,这样点数一共只有 \(2n+2m\) 个,注意这里不要保留 \((y,t)\),因为是死亡所以可以等效地连到后面的第一个点(这点常数要卡好)

我们再来分析一下图的性质,由于单看 \(A,B\),都是时间单调的图,而第二类边有只会带来 \(A\rightarrow B\) 的边,所以说这是一个拓扑图,所以至少存在一组解(全都是死亡状态),并且不存在某个点都是覆盖点 \(x\)\(inv(x)\) 的情况。

现在我们的问题是判断两个生存状态是否能共存,我们可以判断强制选取这两个点之后是否还有解。利用最小字典序的结论:如果当前局面不出现矛盾,那么如果原来有解现在就一定有解。

计算 \(\sum_{1\leq i\leq n,i\not=x} live(x,i)\) 我们可以先把 \(x\)(状态 \(A(x,T+1)\) 简记为 \(x\))选取了,那么考虑 \(x\) 可以到达的死亡状态的集合是 \(s\),那么要求 \(i\not\in S\),此外还要求 \(i\) 不能是初始必死,这样就可以保证当前局面不出现矛盾了。

\(\tt dag\) 上要处理出一个点能到达的点集,可以用 \(\tt bitset\),由于空间限制我们每 \(1000\) 个简单做一次,这样 \(\tt bitset\) 的大小就只用开成 \(1000\) 了,时间复杂度 \(O(\frac{nm}{w})\)

#include <cstdio>
#include <vector>
#include <bitset>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 300005;
const int N = 1005;
#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;
}
void write(int x)
{
	if(x>=10) write(x/10);
	putchar(x%10+'0');
}
int k,n,m,ty[M],t[M],x[M],y[M];
int vis[M],s[M],f[M],ans[M],die[M],in[M];
bitset<N> dp[M];vector<int> v[M],g[M];
int id(int i,int j) {return s[i-1]+j+1;}
//x<<1 alive ; x<<1|1 dead
void add(int x,int y) {g[x].pb(y);g[y^1].pb(x^1);}
//something that I mistake x^1 with x .....
void build()
{
	for(int i=1;i<=n;i++) v[i].pb(k+1);
	for(int i=1;i<=n;i++) s[i]=s[i-1]+v[i].size();
	for(int i=1;i<=n;i++)
	{
		sort(v[i].begin(),v[i].end());
		int len=v[i].size();
		for(int j=0;j+1<len;j++)
			add(id(i,j+1)<<1,id(i,j)<<1);
	}
	for(int i=1;i<=m;i++)
	{
		int p1=lower_bound(v[x[i]].begin()
		,v[x[i]].end(),t[i])-v[x[i]].begin();
		int p2=lower_bound(v[y[i]].begin()
		,v[y[i]].end(),t[i]+(!ty[i]))-v[y[i]].begin();
		if(!ty[i])
			add(id(x[i],p1)<<1|1,id(y[i],p2)<<1|1);
		else
			add(id(x[i],p1)<<1,id(y[i],p2)<<1|1);
	}
	for(int i=1;i<=n;i++)
		f[i]=id(i,v[i].size()-1)<<1;
}
void dfs(int u)
{
	if(vis[u]) return ;vis[u]=1;
	if(!in[u]) dp[u].reset();
	for(int v:g[u])
		dfs(v),dp[u]=dp[u]|dp[v];
}
void work()
{
	for(int l=1,r;l<=n;l+=N)
	{
		r=min(l+N-1,n);
		memset(in,0,sizeof in);
		memset(vis,0,sizeof vis);
		for(int i=l;i<=r;i++)
			in[f[i]|1]=1,dp[f[i]|1].set(i-l);
		for(int i=1;i<=n;i++) dfs(f[i]);
		bitset<N> ban;
		for(int i=l;i<=r;i++) if(dp[f[i]][i-l])
			die[i]=1,ban[i-l]=1;//then i must die
		for(int i=1;i<=n;i++)
			ans[i]+=r-l+1-(ban|dp[f[i]]).count();
	}
	for(int i=1;i<=n;i++)
		write(die[i]?0:ans[i]-1),putchar(' ');
}
signed main()
{
	k=read();n=read();m=read();
	for(int i=1;i<=m;i++)
	{
		ty[i]=read();t[i]=read();x[i]=read();y[i]=read();
		v[x[i]].pb(t[i]);//the crucial points
	}
	build();work();
}
posted @ 2022-02-19 09:15  C202044zxy  阅读(398)  评论(0编辑  收藏  举报