【瞎口胡】线段树分治

线段树分治是一种优秀的时间轴分治算法。

例题 1 Luogu P5787 二分图

题意

有一张 \(n\) 个点 \(m\) 条边的无向图,第 \(i\) 条边在 \(x\) 时刻出现 \(y\) 时刻消失,询问时间段 \(1 \sim k\) 这张图是不是二分图。

时间段 \(x\) 的定义是时刻 \(x-1\)\(x\) 这一段时间。

\(1 \leq n,k \leq 10^5, m \leq 2 \times 10^5,1 \leq x,y \leq k\)

题解

容易观察到,在这道题中,我们要维护的信息易加不易减。线段树分治可以很好地避免这个问题。

对时间建线段树,一条边的出现和消失就是区间修改。每个节点挂一个 std::vector 就行。

假设当前要询问的时间是 \(x\)。在线段树上从根节点往下走到 \(x\) 对应的叶节点,并将沿途经过所有节点中存下的边加入图中。

用扩展域并查集维护。处理一条边 \((u,v)\) 时,在并查集中将点 \((u,v')\)\((v,u')\) 连边。如果某个点 \(i\)\(i'\) 在并查集中连通,则该图不是二分图。这是因为二分图的判定定理:一个图是二分图当且仅当图中不存在奇环。考虑一条边一定从不带 \('\) 的点连向带 \('\) 的点,如果 \(i\)\(i'\) 在并查集中连通,那么一定经过了奇数条边,这样图中就存在一个奇环;反之亦然。

但我们不能直接进行 \(k\) 次查询:每次查询需要清空数组,复杂度退化为了 \(O(kn)\)。如果使用可撤销并查集就可以解决这一问题。具体来讲,在并查集连边时将这一条边两个端点连边前的 \(fa\) 记录下来,存放在栈中,在退出当前节点时弹栈复原信息即可。同时,我们查询完当前节点的左子树时不退出,而是直接进入右子树查询。

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

# include <bits/stdc++.h>

const int N=100010,INF=0x3f3f3f3f;
struct Node{
	int u,sizeu,v,sizev;
};
typedef std::pair <int,int> pii;
pii edge[N<<1];
std::vector <int> tree[N<<2];
int n,m,tlim;
int f[N<<1],size[N<<1];
std::stack <Node> sta;

inline int read(void){
	int res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-')f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}
inline int lc(int x){
	return x<<1;
}
inline int rc(int x){
	return x<<1|1;
}
inline int geta(pii x){
	return x.first;
}
inline int getb(pii x){
	return x.second;
}
void change(int k,int l,int r,int L,int R,int x){
	if(L<=l&&r<=R){
		tree[k].push_back(x);
		return;
	}
	int mid=(l+r)>>1;
	if(L<=mid)
		change(lc(k),l,mid,L,R,x);
	if(mid<R)
		change(rc(k),mid+1,r,L,R,x);
	return;
}
inline int find(int x){
	return (f[x]==x)?x:find(f[x]); 
}
inline void Union(int u,int v){
	u=find(u),v=find(v);
	if(size[u]>size[v])
		std::swap(u,v);
	sta.push((Node){u,size[u],v,size[v]});
	f[u]=v,size[v]+=size[u];
	return;
}
void calc(int k,int l,int r){
	bool flag=true;
	int now=sta.size();
	for(int i=0;i<(int)tree[k].size();++i){
		int u=geta(edge[tree[k][i]]),v=getb(edge[tree[k][i]]);
		if(find(u)==find(v)){
			for(int p=l;p<=r;++p)
				puts("No");
			flag=false;
			break;
		}
		Union(u,v+n),Union(v,u+n);
	}
	int mid=(l+r)>>1;
	if(flag){
		if(l==r)
			puts("Yes");
		else
			calc(lc(k),l,mid),calc(rc(k),mid+1,r);
	}
	while((int)sta.size()>now){
		Node val=sta.top();
		sta.pop();
		f[val.u]=val.u,f[val.v]=val.v,size[val.u]=val.sizeu,size[val.v]=val.sizev;
	}
	return;
}
int main(void){
	n=read(),m=read(),tlim=read();
	for(int i=1;i<=2*n;++i){
		f[i]=i,size[i]=1;
	}
	for(int i=1,l,r;i<=m;++i){
		edge[i].first=read(),edge[i].second=read();
		l=read(),r=read();
		if(l!=r)
			change(1,1,tlim,l+1,r,i);
	}
	calc(1,1,tlim);
	return 0;
}

例题 2 Luogu P4141 消失之物

题意

有一个 \(n\) 个物品和一个背包。问对于每一个物品,其消失后装满容量为 \(1 \sim m\) 的背包的方案数。

\(1 \leq n,m \leq 2000\)

题解

可以看成是物品 \(i\)\([1,i-1]\)\([i+1,n]\) 出现。

当然,有一种更简便的写法:查询左区间就把右区间的所有物品加入背包,查询结束之后撤销。查询右区间同理。

# include <bits/stdc++.h>

const int N=2010,INF=0x3f3f3f3f;

int f[20][N];
int a[N];
int n,m;

inline int read(void){
	int res,f=1;
	char c;
	while((c=getchar())<'0'||c>'9')
		if(c=='-')f=-1;
	res=c-48;
	while((c=getchar())>='0'&&c<='9')
		res=res*10+c-48;
	return res*f;
}
void solve(int l,int r,int dep){
	if(l==r){
		for(int i=1;i<=m;++i){
			printf("%d",f[dep][i]);
		}
		puts("");
		return;
	}
	int mid=(l+r)>>1;
	for(int i=0;i<=m;++i)
		f[dep+1][i]=f[dep][i];
	for(int i=mid+1;i<=r;++i){
		for(int j=m;j>=a[i];--j){
			f[dep+1][j]=(f[dep+1][j]+f[dep+1][j-a[i]])%10;
		}
	}
	solve(l,mid,dep+1);
	for(int i=0;i<=m;++i)
		f[dep+1][i]=f[dep][i];
	for(int i=l;i<=mid;++i){
		for(int j=m;j>=a[i];--j){
			f[dep+1][j]=(f[dep+1][j]+f[dep+1][j-a[i]])%10;
		}
	}
	solve(mid+1,r,dep+1);
	return;
}
int main(void){
	n=read(),m=read();
	for(int i=1;i<=n;++i){
		a[i]=read();
	}
	f[1][0]=1;
	solve(1,n,1);
	return 0;
}
原文地址:https://www.cnblogs.com/liuzongxin/p/15256550.html