仙人掌基础例题

仙人掌

前置知识:Tarjan,圆方树

定义

  • 仙人掌:每条边在不超过一个简单环中的无向图

一般用圆方树处理仙人掌问题,例如 圆方树dp 或 圆方树树剖

例题1:小C的独立集(BZOJ4316)

description

求仙人掌的最大独立集(在仙人掌中选出一些两两没有连边的点,求最大点数)
\(n \leq 50000, m \leq 60000\)

Solution

如果这是个树,那就是个最基础的树形 \(dp\)
没有必要建出圆方树区分圆点和方点,直接在 \(Tarjan\) 的时候 \(dp\)
对于圆点与圆点的边,直接按照树的转移
对于在环上的,我们不操作,然后统计环上每个点的 \(dp\) 值,只需要给环的顶端节点更新 \(dp\) 值,环上别的点就不用更新了

\(f[i][0/1]\) 表示i选或不选,子树中的最大独立集。
考虑如何计算一个环的答案,就是从环底端往上跳,边跳边合并
设顶端节点 \(x\) ,底端节点 \(y\)
\(x\) 不选, \(y\) 选不选都行,然后顺着环往上,跟树一样转移就行,然后更新 \(f[x][0]\)
\(x\) 选, \(y\) 必须选,同理去更新 \(f[x][1]\)

复杂度 \(O(n)\)

Code

#include <cmath>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define rint register int
using namespace std;

const int maxn = 50000 + 10;
const int maxm = 60000 + 10;
const int inf = 0x3f3f3f3f;
int n, m, cnt, head[maxn], fa[maxn], f[maxn][2];
int low[maxn], dfn[maxn], Time;

struct Edge {
	int to, nxt;
}e[maxm << 1];

int read(rint x = 0, register bool f = 0, register char ch = getchar()) {
	for(;ch < '0' || ch > '9';ch = getchar()) f = ch == '-';
	for(;ch >= '0' && ch <= '9';ch = getchar()) x = (x << 3) + (x << 1) + (ch & 15);
	return f ? -x : x ;
}

void dp(rint x, rint y) {
	rint f0 = 0, f1 = 0; 
	for(rint i = y;i != x;i = fa[i]) {
		rint res0 = f[i][0] + f0; // 当前点不选
		rint res1 = f[i][1] + f1; // 选
		f0 = max(res0, res1); // 对下个点不选的贡献
		f1 = res0; // 对下个点选的贡献
	}
	f[x][0] += f0;
	f0 = 0, f1 = -inf;
	for(rint i = y;i != x;i = fa[i]) {
		rint res0 = f[i][0] + f0;
		rint res1 = f[i][1] + f1;
		f0 = max(res0, res1);
		f1 = res0;
	}
	f[x][1] += f1;
}

void dfs(rint x, rint prt) {
	f[x][1] = 1;
	fa[x] = prt;
	dfn[x] = low[x] = ++Time;
	for(rint i = head[x], y;i;i = e[i].nxt) {
		y = e[i].to;
		if(!dfn[y]) {
			dfs(y, x);
			low[x] = min(low[x], low[y]);
		}
		else if(y != prt) low[x] = min(low[x], dfn[y]);
		if(low[y] > dfn[x]) {
			f[x][0] += max(f[y][0], f[y][1]);
			f[x][1] += f[y][0];
		}
	}
	for(rint i = head[x], y;i;i = e[i].nxt) {
		y = e[i].to;
		if(dfn[y] > dfn[x] && fa[y] != x) dp(x, y);
	}
}

int main() {
	n = read(), m = read();
	for(rint i = 1;i <= m; ++i) {
		rint x = read(), y = read();
		e[++cnt] = (Edge){y, head[x]}, head[x] = cnt;
		e[++cnt] = (Edge){x, head[y]}, head[y] = cnt;
	}
	dfs(1, 0);
	return printf("%d\n", max(f[1][0], f[1][1])), 0;
}

例题2:cactus仙人掌图(BZOJ1023)

Description

求仙人掌直径
$ 1 \leq n \leq 50000, 0 \leq m \leq 10000$

Solution

仙人掌 \(dp\) ,树边转移和树形 \(dp\) 求直径一样,记录个最长链,遍历儿子时,更新 \(ans\)\(dp\) 值就好
每次对于环的 \(dp\) 只需要把环顶端的 \(dp\) 值更新好就行
首先对于环上的点 \(x\)\(y\) ,用 \(x\)\(y\) 的最短距离与 \(f[x]\) , \(f[y]\) 加和去更新 \(ans\)
然后扫一遍环上的点,更新顶端节点的 \(dp\)

更新 \(ans\) 的时候枚举环上的一个点,用这个点以及距离它不超过环长/2的点的贡献更新 \(ans\) ,用单调队列维护就行了。
因为这是一个环,枚举 \(2\) 圈就可以算上开头处一个点和结尾处一个点匹配的贡献了。
复杂度 \(O(n)\)

Code

#include <cmath>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define rint register int
#define ll long long
using namespace std;

const int maxn = 50000 * 2 + 10;
const int maxm = 2e7 + 10;
int n, m, cnt, head[maxn], fa[maxn], dep[maxn];
int Time, dfn[maxn], low[maxn];
int l, r, ans, q[maxn], f[maxn], a[maxn];

struct Edge {
	int to, nxt;
}e[maxm];

int read(rint x = 0, register bool f = 0, register char ch = getchar()) {
	for(;ch < '0' || ch > '9';ch = getchar()) f = ch == '-';
	for(;ch >= '0' && ch <= '9';ch = getchar()) x = (x << 3) + (x << 1) + (ch & 15);
	return f ? -x : x;
}

void solve(rint x, rint y) {
	rint tot = 0;
	for(rint i = y;i != x;i = fa[i]) a[++tot] = i;
	a[++tot] = x;
	for(rint i = 1;i <= tot; ++i) a[i + tot] = a[i];
	l = 1, r = 0;
	for(rint i = 1;i <= (tot << 1); ++i) {
		while(l <= r && i - q[l] > (tot >> 1)) ++l;
		while(l <= r && f[a[i]] - i > f[a[q[r]]] - q[r]) --r;
		if(l <= r) ans = max(ans, f[a[i]] + f[a[q[l]]] + i - q[l]);
		q[++r] = i;
	}
	for(rint i = y;i != x;i = fa[i]) {
		f[x] = max(f[x], f[i] + min(dep[i] - dep[x], dep[y] + 1 - dep[i]));
	}
}

void dfs(rint x, rint prt) {
	fa[x] = prt;
	dep[x] = dep[prt] + 1;
	dfn[x] = low[x] = ++Time;
	for(rint i = head[x], y;i;i = e[i].nxt) {
		y = e[i].to;
		if(!dfn[y]) {
			dfs(y, x);
			low[x] = min(low[x], low[y]);
		}
		else if(y != prt) low[x] = min(low[x], dfn[y]);
		if(low[y] > dfn[x]) {
			ans = max(ans, f[x] + f[y] + 1);
			f[x] = max(f[x], f[y] + 1);
		}
	}
	for(rint i = head[x], y;i;i = e[i].nxt) {
		y = e[i].to;
		if(dfn[y] > dfn[x] && fa[y] != x) solve(x, y);
	}
}

int main() {
	n = read(), m = read();
	for(rint i = 1;i <= m; ++i) {
		rint k = read(), x = read(), y;
		while(--k) {
			y = read();
			e[++cnt] = (Edge){y, head[x]}, head[x] = cnt;
			e[++cnt] = (Edge){x, head[y]}, head[y] = cnt;
			x = y;
		}
	}
	dfs(1, 0);
	return printf("%d\n", ans), 0;
}
  • 仙人掌 \(dp\) 的时候,树边直接转移,对于环去单独考虑,更新出环顶端的 \(dp\)

例题3:最短路(BZOJ2125)

Description

询问仙人掌上任意两点最短路, \(q\) 组询问。
\(N \leq 10000\) , \(Q \leq 10000\)

Solution

建圆方树,对于所有点数大于 \(1\) 的点双(环)建方点。
给圆方树赋上边权,树剖求 \(lca\)

圆点与圆点的边,边权与原图相同,为 \(1\)
圆点与方点的边,边权为仙人掌上该点到达环上深度最小的点的最短距离,因为有 \(2\) 条路径,我们记短的那条。

然后查询的时候找到圆方树上的 \(lca\) ,如果 \(lca\)是圆点,直接差分计算答案
否则说明询问的两点的祖先在一个环上,对于上面环上这一段,跳到环上单独算
复杂度 \(O(nlogn)\)

Code

#include <cmath>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define rint register int
using namespace std;

const int maxn = 2e4 + 10;
int c[maxn], rf[maxn];
int n, m, q, scc, top, Time, sta[maxn], dfn[maxn], low[maxn];
int siz[maxn], dep[maxn], dis[maxn], son[maxn], fa[maxn], Top[maxn];
bool rev[maxn];

struct Link {
	int cnt, head[maxn];
	struct Edge {
		int to, nxt, val;
	}e[maxn << 1];
	void add(rint x, rint y, rint z) {
		e[++cnt] = (Edge){y, head[x], z}, head[x] = cnt;
	}
} G, T;

int read(rint x = 0, register bool f = 0, register char ch = getchar()) {
	for(;ch < '0' || ch > '9';ch = getchar()) f = ch == '-';
	for(;ch >= '0' && ch <= '9';ch = getchar()) x = (x << 3) + (x << 1) + (ch & 15);
	return f ? -x : x;
}

void solve(rint x, rint y, rint v) {
	rint len = v;
	++scc, top = dep[y]-dep[x]+1;
	for(rint i = y, j = top;i != x;i = fa[i]) {
		sta[j--] = i;
		len += dis[i]-dis[fa[i]];
	}
	sta[1] = x, c[scc] = len;
	for(rint i = 1, now = 0;i <= top; ++i) {
		rint d = min(len - now, now);
		rint x = sta[i];
		rev[x] = (d == now);
		T.add(x, scc, d);
		T.add(scc, x, d);
		now += dis[sta[i+1]]-dis[x];
	}
}

void tar(rint x, rint prt) {
	fa[x] = prt;
	dep[x] = dep[prt] + 1;
	dfn[x] = low[x] = ++Time;
	for(rint i = G.head[x];i;i = G.e[i].nxt) {
		rint y = G.e[i].to;
		if(y == prt) continue;
		if(!dfn[y]) {
			dis[y] = dis[x] + G.e[i].val;
			tar(y, x);
			low[x] = min(low[x], low[y]);
		}
		else low[x] = min(low[x], dfn[y]);
		if(low[y] > dfn[x]) T.add(x, y, G.e[i].val);
	}
	for(rint i = G.head[x];i;i = G.e[i].nxt) {
		rint y = G.e[i].to;
		if(dfn[y] > dfn[x] && fa[y] != x) solve(x, y, G.e[i].val);
	}
}

void dfs1(rint x, rint prt) {
	siz[x] = 1;
	fa[x] = prt;
	dep[x] = dep[prt] + 1;
	for(rint i = T.head[x];i;i = T.e[i].nxt) {
		rint y = T.e[i].to;
		if(y == prt) continue;
		dis[y] = dis[x] + T.e[i].val;
		dfs1(y, x);
		siz[x] += siz[y];
		if(!son[x] || siz[y] > siz[son[x]]) son[x] = y;
	}
}

void dfs2(rint x, rint tp) {
	Top[x] = tp;
	dfn[x] = ++Time;
	rf[Time] = x;
	if(son[x]) dfs2(son[x], tp);
	for(rint i = T.head[x];i;i = T.e[i].nxt) {
		rint y = T.e[i].to;
		if(y != son[x] && y != fa[x]) dfs2(y, y);
	}
}

int lca(rint x, rint y) {
	while(Top[x] != Top[y]) {
		if(dep[Top[x]] < dep[Top[y]]) swap(x, y);
		x = fa[Top[x]];
	}
	return dep[x] < dep[y] ? x : y;
}

int jump(rint x, rint to) {
	rint lst = x;
	while(Top[x] != Top[to]) {
		lst = Top[x];
		x = fa[lst];
	}
	return x != to ? rf[dfn[to]+1] : lst;
}

int query(rint x, rint y) {
	rint lc = lca(x, y);
	if(lc <= n) return dis[x] + dis[y] - dis[lc] * 2;
	rint xx = jump(x, lc), yy = jump(y, lc);
	rint lx = dis[xx] - dis[lc], ly = dis[yy] - dis[lc];
	if(!rev[xx]) lx = c[lc] - lx; //!
	if(!rev[yy]) ly = c[lc] - ly; //!
	return dis[x] - dis[xx] + dis[y] - dis[yy] + min(abs(lx-ly), c[lc]-abs(lx-ly));
}

int main() {
	scc = n = read(), m = read(), q = read();
	for(rint i = 1, x, y, z;i <= m; ++i) {
		x = read(), y = read(), z = read();
		G.add(x, y, z);
		G.add(y, x, z);
	}
	tar(1, 0);
	Time = 0, dfs1(1, 0), dfs2(1, 1);
	for(rint i = 1;i <= q; ++i) {
		rint x = read(), y = read();
		printf("%d\n", query(x, y));
	}
	return 0;
}

例题4:cac

Descirprion

给定一棵仙人掌, \(q\) 次操作,2种操作类型:

  • 任选 \(2\)\(x\) , \(y\) ,将 \(x\)\(y\) 之间的所有简单路径上的点加上权值 \(v\)
  • 询问点 \(x\) 的权值,对 \(998244353\) 取模

\(n \leq 3*10^5\) , \(m \leq 4*10^5\) , \(q \leq 10^5\) , \(v < 10^7\)

Solution

如果简单路径经过了一个点双中大于一个点,那么这个点双全应加上权值
建出圆方树,因为圆点与方点是相间的,对于修改操作,如果 \(x\)\(y\) 在圆方树上的路径经过了方点,那么这个方点代表的点双应该全部加权
可以维护方点权值,代表圆方树上该点子树中与其相邻的圆点的权值。
回答询问时,统计圆方树上该点父亲的权值。

但是在 \(lca\) 处要处理一下:
如果 \(lca\) 是圆点,那么其也要加权。
如果 \(lca\) 是方点,那么其父亲也要加权。

区间修改,单点查询,树剖树状数组维护,复杂度 \(O(nlog^2n)\)

Code

#include <cmath>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define ll long long
using namespace std;

const int maxn=6e5+10,mod=998244353;
int n,m,q,Time,top,dcc,cnt,Cnt,head[maxn],Head[maxn];
int sta[maxn],v[maxn],low[maxn],dfn[maxn],siz[maxn],son[maxn],Top[maxn],fa[maxn],dep[maxn];
int t[maxn];
struct Edge{ int to,nxt; }e[maxn*2],E[maxn*2];

int read(int x=0,bool f=0,char ch=getchar()){
	for(;ch<'0' || ch>'9';ch=getchar()) f=ch=='-';
	for(;ch>='0'&&ch<='9';ch=getchar()) x=(x<<3)+(x<<1)+(ch&15);
	return f?-x:x;

}

void add(int x,int y){ e[++cnt]=(Edge){y,head[x]},head[x]=cnt; }
void Add(int x,int y){ E[++Cnt]=(Edge){y,Head[x]},Head[x]=Cnt; }

void T(int x){
	dfn[x]=low[x]=++Time;
	sta[++top]=x;	
	for(int i=head[x];i;i=e[i].nxt){
		int y=e[i].to;
		if(!dfn[y]){
			T(y);
			low[x]=min(low[x],low[y]);
			if(dfn[x]==low[y]){
				++dcc;
				int now;
				do{
					now=sta[top--];
					Add(now,dcc),Add(dcc,now);
				}while(now!=y);
				Add(x,dcc),Add(dcc,x);
			}
		}
		else low[x]=min(low[x],dfn[y]);
	}
}

void dfs1(int x,int prt){
	fa[x]=prt,dep[x]=dep[prt]+1,siz[x]=1;
	for(int i=Head[x];i;i=E[i].nxt){
		int y=E[i].to;
		if(y==prt) continue;
		dfs1(y,x);
		siz[x]+=siz[y];
		if(!son[x] || siz[y]>siz[son[x]]) son[x]=y;
	}
}

void dfs2(int x,int tp){
	dfn[x]=(x>n)?++Time:Time,Top[x]=tp;
	if(son[x]) dfs2(son[x],tp);
	for(int i=Head[x];i;i=E[i].nxt){
		int y=E[i].to;
		if(y!=fa[x]&&y!=son[x]) dfs2(y,y);
	}
}

void Tadd(int x,int y,int v){
	if(x>y) return;
	for(;x<=Time;x+=(x&-x)) (t[x]+=v)%=mod;
	for(++y;y<=Time;y+=(y&-y)) (t[y]+=mod-v)%=mod;
}

int Ask(int x,int ret=0){
	for(;x;x-=(x&-x)) (ret+=t[x])%=mod;
	return ret;
}

void Modify(int x,int y,int val){
	while(Top[x]!=Top[y]){
		if(dep[Top[x]]<dep[Top[y]]) swap(x,y);
		if(Top[x]>n) Tadd(dfn[Top[x]],dfn[x],val);
		else Tadd(dfn[Top[x]]+1,dfn[x],val);
		x=fa[Top[x]];
	}
	if(dep[x]<dep[y]) swap(x,y);
	if(y>n) Tadd(dfn[y],dfn[x],val),(v[fa[y]]+=val)%=mod;
	else Tadd(dfn[y]+1,dfn[x],val),(v[y]+=val)%=mod;
}

int main(){
	freopen("cac.in","r",stdin);
	freopen("cac.out","w",stdout);
	dcc=n=read(),m=read(),q=read();
	for(int i=1;i<=m;++i){
		int x=read(),y=read();
		add(x,y),add(y,x);
	}
	for(int i=1;i<=n;++i) if(!dfn[i]) T(i);
	memset(dfn,0,sizeof(dfn)),Time=0;
	dfs1(1,0),dfs2(1,1);
	while(q-->0){
		int opt=read();
		if(!opt){
			int x=read(),y=read(),val=read();
			Modify(x,y,val);
		}
		else{
			int x=read(),ans=v[x];
			if(fa[x]) (ans+=Ask(dfn[fa[x]]))%=mod;
			printf("%d\n",ans);
		}
	}
	return 0;
}
posted @ 2021-04-13 11:44  liuzhaoxu  阅读(42)  评论(0编辑  收藏  举报