仙人掌基础例题
仙人掌
前置知识: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;
}