什么是仙人掌

仙人掌是一种带刺的图, 它可以让你浑身都不舒服
仙人掌是一种连通图, 没有一条边同时属于两个简单环. 形象地表示一下, 就又要祭出这张经典图了:
仙人掌
那么仙人掌有什么特点, 使得它被毒瘤出题人钟爱呢? 这就要谈到种植仙人掌的姿势了:
准备一个瓦盆, 在里面填上沙壤土, 将仙人掌种进去, 平时少浇水多晒太阳

圆方树

圆方树, 即将仙人掌化成一棵树, 而树上问题非常经典, 于是出题人就可以把一些树上问题搬到仙人掌上, 然后来恶心选手辣!
圆方树即原仙人掌中的点为圆点, 对于每一个环, 添加一个方点, 方点与所有环上的圆点相连, 没有两个方点会相连. 如图:
灵魂画手litble
当然了, 在圆方树上解决问题的时候, 一定要特别考虑方点对于问题带来的影响.
如何种植圆方树呢? 跑 tarjan 双连通分量找环先考虑不在环上的点再考虑在环上的, 正当 litble 这么想着的时候, 一位不愿透露姓名的 Cai 大佬跑过来, 大喊:” 打圆方树居然用 tarjan?! 你是人吗?”
失去做人资格的 litble 又向 Cai 大佬请教了一发,get 了 Cai 大佬由于打挂了 tarjan 很不爽而发明的不用 tarjan 的构造圆方树的方法:
1. 遍历仙人掌,同时打上时间戳
2. 假如遇到了一个时间戳比在当前点 x 小的点,说明发现了一个环,暴力将该环打上 “是环” 的标记,同时环顶要打上不同的标记
3. 继续遍历仙人掌,并标记每个点的 “父亲” 即来时的节点
4. 回溯时,如果发现该点有环上信息标记,则不用将该点与父亲连边,取消标记。如果是环顶,则暴力处理环的一些相关内容,如建立方点等。

void btree(int x,int las) {//构建圆方树,las: 记录来时的边
    vis[x]=++tim;//打时间戳
    for(int i=hh[x];i;i=nn[i]) 
        if((i^1)!=las) {
        if(vis[tt[i]]&&vis[tt[i]]>vis[x]) continue;
        if(vis[tt[i]]) ks[tt[i]]=ww[i],mc(x,tt[i]);//ks,kl:存环上的距离信息,mc: 暴力标记环
        if(!vis[tt[i]]) pre[tt[i]]=x,kl[tt[i]]=ww[i],btree(tt[i],i);//递归处理
        if(cir[x]==-1) {cir[x]=0;continue;}
        if(cir[x]) {dc(cir[x],x),cir[x]=0;continue;}//对环进行处理
        link(x,tt[i],ww[i]);//连边
    }
}

例题:bzoj2125。
圆点和圆点之间的边权就是原图的边权,圆点到儿子方点的距离为 0,方点到儿子圆点的距离为原仙人掌上该圆点到方点的父亲(一个圆点)的环上最短距离。
然后对于每组询问求 LCA,如果 LCA 是圆点,距离很好弄,是方点,就看在求 LCA 的路径上经过的那两个圆点,通过环上前缀和的方式求出它们之间的最短距离。

#include<bits/stdc++.h>
using namespace std;
#define LL long long
int read() {
    int q=0;char ch=' ';
    while(ch<'0'||ch>'9') ch=getchar();
    while(ch>='0'&&ch<='9') q=q*10+ch-'0',ch=getchar();
    return q;
}
const int N=10005,M=50005,NN=20005;;
int n,m,Q,t0=1,tim,sz,tot;
int hh[N],tt[M],nn[M],ww[M],vis[N],pre[N],cir[N],kl[N],ks[N];
int h[NN],to[NN],ne[NN],w[NN];LL s[NN];
void add(int x,int y,int z) {tt[++t0]=y,nn[t0]=hh[x],hh[x]=t0,ww[t0]=z;}
void link(int x,int y,int z) {to[++tot]=y,ne[tot]=h[x],h[x]=tot,w[tot]=z;}
void mc(int x,int y) {cir[y]=x;while(x!=y) cir[x]=-1,x=pre[x];}//对环做标记
void dc(int x,int y) {//处理环,建立方点和环上前缀和
    ++sz,link(y,sz,0);int sum=ks[y],t=x;
    while(x!=y) s[x]=sum,sum+=kl[x],x=pre[x];
    s[sz]=sum;
    while(t!=y) link(sz,t,min(s[t],s[sz]-s[t])),t=pre[t];
}
void btree(int x,int las) {//构建圆方树
    vis[x]=++tim;
    for(int i=hh[x];i;i=nn[i]) 
        if((i^1)!=las) {
        if(vis[tt[i]]&&vis[tt[i]]>vis[x]) continue;
        if(vis[tt[i]]) ks[tt[i]]=ww[i],mc(x,tt[i]);//ks,kl:存环上的距离信息
        if(!vis[tt[i]]) pre[tt[i]]=x,kl[tt[i]]=ww[i],btree(tt[i],i);
        if(cir[x]==-1) {cir[x]=0;continue;}
        if(cir[x]) {dc(cir[x],x),cir[x]=0;continue;}
        link(x,tt[i],ww[i]);
    }
}
LL dis[NN];int f[NN][16],dep[NN];
void dfs(int x,int las) {
    f[x][0]=las,dep[x]=dep[las]+1;
    for(int i=1;i<=15;++i) f[x][i]=f[f[x][i-1]][i-1];
    for(int i=h[x];i;i=ne[i])
        if(to[i]!=las) dis[to[i]]=dis[x]+(LL)w[i],dfs(to[i],x);
}
int lca(int x,int y,int &ka,int &kb) {
    if(dep[x]<dep[y]) swap(x,y);
    for(int i=15;i>=0;--i) if(dep[f[x][i]]>=dep[y]) x=f[x][i];
    if(x==y) return x;
    for(int i=15;i>=0;--i)
        if(f[x][i]!=f[y][i]) x=f[x][i],y=f[y][i];
    ka=x,kb=y;return f[x][0];
}
int main() {
    int x,y,z,ka,kb;
    n=read(),m=read(),Q=read();
    for(int i=1;i<=m;++i) {
        x=read(),y=read(),z=read();
        add(x,y,z),add(y,x,z);
    }
    sz=n,btree(1,0),dfs(1,0);
    while(Q--) {
        x=read(),y=read(),z=lca(x,y,ka,kb);
        if(z<=n) printf("%lld\n",dis[x]+dis[y]-dis[z]*2);
        else {//如果 lca 是方点
            LL ans=dis[x]+dis[y]-dis[ka]-dis[kb];
            ans+=min(abs(s[ka]-s[kb]),s[z]-abs(s[ka]-s[kb]));
            printf("%lld\n",ans);
        }
    }
    return 0;
}

仙人掌生成器

在打第一道仙人掌题的时候, 蒟蒻 litble 就各种 GG, 于是膜了一发仙人掌生成器.VFK 应该在他的 uojblog 里说过一种生成仙人掌的方法, 但是还有一种方法是:
构建一棵树, 将若干不相邻的点改成方点, 然后把圆方树还原成一棵仙人掌.
以下是上面那道例题的数据生成器.

#include<bits/stdc++.h>
using namespace std;
const int N=100005,M=200005;
int n,m,tot,js,tn,q;
int f[N],bj[N],h[N],to[N],ne[N],u[M],v[M],pos[N];
void add(int x,int y) {to[++tot]=y,ne[tot]=h[x],h[x]=tot;}
void dfs(int x,int las) {
    if(!bj[x]) pos[x]=++tn;//点的重标号
    int tmp=las;
    for(int i=h[x];i;i=ne[i]) {
        if(to[i]==las) continue;
        dfs(to[i],x);
        if(!bj[x]&&!bj[to[i]]) u[++m]=pos[x],v[m]=pos[to[i]];//不在环上的边
        if(bj[x]) u[++m]=pos[tmp],v[m]=pos[to[i]],tmp=to[i];//对于方点, 搞出环
    }
    if(bj[x]&&tmp!=las) u[++m]=pos[tmp],v[m]=pos[las];
}
int check(int x) {
    if(x==1) return 0;
    if(bj[f[x]]) return 0;
    for(int i=h[x];i;i=ne[i])
        if(bj[to[i]]) return 0;
    return 1;
}
int main()
{
    srand(time(0));
    n=rand()%12+3,js=rand()%(n-2)+1,q=rand()%10+1;//n: 树的点数,js: 方点个数
    for(int i=2;i<=n;++i) f[i]=rand()%(i-1)+1,add(f[i],i);//生成一棵树
    for(int i=1;i<=js;++i) {
        int x=rand()%(n-1)+2;//圆方树的根一定要是圆点
        if(check(x)) bj[x]=1;//check: 判断是否可以是方点
    }
    dfs(1,0);
    printf("%d %d %d\n",tn,m,q);//输出结果
    for(int i=1;i<=m;++i) printf("%d %d %d\n",u[i],v[i],rand()%10+1);
    for(int i=1;i<=q;++i) {
        int x=rand()%tn+1,y=rand()%tn+1;
        while(y==x) y=rand()%tn+1;
        printf("%d %d\n",x,y);
    }
    return 0;
}

仙人掌的应用

准确的来说,仙人掌只是一种出题人无聊的时候整一整的东西,没有任何实际应用价值,不过,我还说说一说吧。

bzoj4316

圆方树的树形 dp, 可以设 $f(i,0)$: 不选 i 的情况下,i 的子树里最多可以选多少点。$f(i,1)$: 不选 i
对于圆点,就是最普通的树形 dp。对于方点,我们可以先看看这个环,环顶(方点的父亲)的点和某一个与环顶相连的点里,一定有一个不选,那么我们设强制不选环顶或者强制不选环底(即那个与环顶相连的点),跑两次 dp 更新方点父亲的 dp 值。

#include<bits/stdc++.h>
using namespace std;
#define LL long long
int read() {
    int q=0;char ch=' ';
    while(ch<'0'||ch>'9') ch=getchar();
    while(ch>='0'&&ch<='9') q=q*10+ch-'0',ch=getchar();
    return q;
}
const int N=50005,M=120005,inf=0x3f3f3f3f;
int h1[N],h2[N<<1],vis[N],cir[N],pre[N];
int f[N<<1][2],g[N<<1][2],st[N<<1];
struct edge{int ne,to;}e1[M],e2[N<<1];
int n,m,t1=1,t2,tim,sz;
void ad1(int x,int y) {e1[++t1].to=y,e1[t1].ne=h1[x],h1[x]=t1;}
void ad2(int x,int y) {e2[++t2].to=y,e2[t2].ne=h2[x],h2[x]=t2;}
void mc(int x,int y) {cir[y]=x;while(x!=y) cir[x]=-1,x=pre[x];}
void dc(int x,int y) {++sz,ad2(y,sz);while(x!=y) ad2(sz,x),x=pre[x];}
void btree(int x,int las) {//建立圆方树
    vis[x]=++tim;
    for(int i=h1[x];i;i=e1[i].ne)
        if((i^1)!=las) {
        if(vis[e1[i].to]&&vis[e1[i].to]>vis[x]) continue;
        if(vis[e1[i].to]) mc(x,e1[i].to);
        if(!vis[e1[i].to]) pre[e1[i].to]=x,btree(e1[i].to,i);
        if(cir[x]==-1) {cir[x]=0;continue;}
        if(cir[x]) {dc(cir[x],x),cir[x]=0;continue;}
        ad2(x,e1[i].to);
    }
}
void dp(int x,int las) {
    if(x<=n) {//圆点直接 dp
        f[x][0]=0,f[x][1]=1;
        for(int i=h2[x];i;i=e2[i].ne) {
            int y=e2[i].to;dp(y,x);
            if(y<=n) f[x][0]+=max(f[y][0],f[y][1]),f[x][1]+=f[y][0];
        }
        return;
    }
    int top=0,t0,t1;//处理方点
    for(int i=h2[x];i;i=e2[i].ne) dp(e2[i].to,x);
    for(int i=h2[x];i;i=e2[i].ne) st[++top]=e2[i].to;//不可与上一句同写
    st[++top]=las,g[st[1]][0]=f[st[1]][0],g[st[1]][1]=-inf;//不选环底
    for(int i=2;i<=top;++i) {
        g[st[i]][0]=f[st[i]][0]+max(g[st[i-1]][0],g[st[i-1]][1]);
        g[st[i]][1]=f[st[i]][1]+g[st[i-1]][0];
    }
    t0=max(f[las][0],g[las][0]),t1=max(f[las][1],g[las][1]);
    g[st[1]][0]=f[st[1]][0],g[st[1]][1]=f[st[1]][1];//不选环顶
    for(int i=2;i<=top;++i) {
        g[st[i]][0]=f[st[i]][0]+max(g[st[i-1]][0],g[st[i-1]][1]);
        g[st[i]][1]=f[st[i]][1]+g[st[i-1]][0];
    }
    f[las][0]=max(t0,g[las][0]),f[las][1]=t1;
}
int main() {
    int x,y; n=read(),m=read();
    for(int i=1;i<=m;++i) x=read(),y=read(),ad1(x,y),ad1(y,x);
    sz=n,btree(1,0),dp(1,0);
    printf("%d\n",max(f[1][0],f[1][1]));
    return 0;
}

bzoj1023

同样也是圆点直接 dp,方点弄个单调队列,考虑环上的两个点之间的距离怎样最近(显然两个环上的点之间有两条路),然后 dp 两次即可。

#include<bits/stdc++.h>
using namespace std;
int read() {
    int q=0;char ch=' ';
    while(ch<'0'||ch>'9') ch=getchar();
    while(ch>='0'&&ch<='9') q=q*10+ch-'0',ch=getchar();
    return q;
}
const int N=50005;
int n,m,t1=1,t2,tim,sz,ans;
int h1[N],h2[N<<1],vis[N],cir[N],pre[N],f[N<<1],st[N<<1];
int Q[N<<1],ss[N<<1];
struct edge{int to,ne;}e1[N<<2],e2[N<<1];
void ad1(int x,int y) {e1[++t1].to=y,e1[t1].ne=h1[x],h1[x]=t1;}
void ad2(int x,int y) {e2[++t2].to=y,e2[t2].ne=h2[x],h2[x]=t2;}
void mc(int x,int y) {cir[y]=x;while(x!=y) cir[x]=-1,x=pre[x];}
void dc(int x,int y) {++sz,ad2(y,sz);while(x!=y) ad2(sz,x),x=pre[x];}
void btree(int x,int las) {
    vis[x]=++tim;
    for(int i=h1[x];i;i=e1[i].ne)
        if((i^1)!=las) {
        if(vis[e1[i].to]&&vis[e1[i].to]>vis[x]) continue;
        if(vis[e1[i].to]) mc(x,e1[i].to);
        if(!vis[e1[i].to]) pre[e1[i].to]=x,btree(e1[i].to,i);
        if(cir[x]==-1) {cir[x]=0;continue;}
        if(cir[x]) {dc(cir[x],x),cir[x]=0;continue;}
        ad2(x,e1[i].to);
    }
}
void dp(int x) {
    if(x<=n) {
        int mx1=0,mx2=0;
        for(int i=h2[x];i;i=e2[i].ne) {
            dp(e2[i].to);
            if(f[e2[i].to]+1>=mx1) mx2=mx1,mx1=f[e2[i].to]+1;
            else if(f[e2[i].to]+1>mx2) mx2=f[e2[i].to]+1;
        }
        ans=max(ans,mx1+mx2),f[x]=mx1;
        return;
    }
    int top=0,he=1,ta=0;
    for(int i=h2[x];i;i=e2[i].ne) dp(e2[i].to);
    for(int i=h2[x];i;i=e2[i].ne) st[++top]=e2[i].to;
    for(int i=1;i<=top;++i) {//方点第一次 dp
        while(he<=ta&&i-Q[he]>top+1-i+Q[he]) ++he;
        if(he<ta) ans=max(ans,f[st[i]]+f[st[Q[he]]]+i-Q[he]);
        while(he<=ta&&f[st[i]]-i>=f[st[Q[ta]]]-Q[ta]) --ta;
        Q[++ta]=i;
    }
    he=1,ta=0;
    for(int i=top;i>=1;--i) {//方点第二次 dp
        while(he<=ta&&i-Q[he]<top+1-i+Q[he]) ++he;
        if(he<ta) ans=max(ans,f[st[i]]+f[st[Q[he]]]+top+1-i+Q[he]);
        while(he<=ta&&f[st[i]]+i>=f[st[Q[ta]]]+Q[ta]) --ta;
        Q[++ta]=i;
    }
    for(int i=1;i<=top/2;++i) f[x]=max(f[x],i+f[st[i]]-1);
    for(int i=top/2+1;i<=top;++i) f[x]=max(f[x],top-i+f[st[i]]);
}
int main() {
    int num,x,y;
    n=read(),m=read();
    for(int i=1;i<=m;++i) {
        num=read();
        for(int j=1;j<=num;++j,y=x)
            {x=read(); if(j!=1) ad1(y,x),ad1(x,y);}
    }
    sz=n,btree(1,0),dp(1);
    printf("%d\n",ans);
    return 0;
}
分类: 文章

litble

苟...苟活者在淡红的血色中,会依稀看见微茫的希望

1 条评论

konnyakuxzy · 2018年1月27日 9:22 下午

Orz

回复 konnyakuxzy 取消回复

Avatar placeholder

您的电子邮箱地址不会被公开。 必填项已用 * 标注