前言
有难度的仙人掌题在近几年也只是在国家集训队水平的比赛里才会出现。
不过,这不是说仙人掌对国集水平以下的选手意义不大:
首先,仙人掌暴力 DP 问题难度并不大,在省选、 NOI 甚至 NOIP 中可能出现;
其次,仙人掌问题的处理能很好锻炼选手的特殊情况全面考虑并正确处理的能力,提升对超长代码的把握;
如图所示:
仙人掌图就是长得像仙人掌的图嘛(我真没看出哪里像了)
定义:对一个无向连通图,任意一条边属于至多一个简单环。
桥边:非环边,就是连接环的那些边;
环边:就是环中的边嘛。
在仙人掌上,父亲和儿子都有节点的和环的之分。
DFS 树解决仙人掌 DP 问题
仙人掌的处理是十分复杂的,这里先从简单的 DFS树开始。
树边:DFS 树中存在的边
非树边:DFS 树中不存在的边
大神们还有什么覆盖之类的定义,参考最后的参考文献。
也就是说环是由多条树边和一条非树边组成的,非树边起到了连接的作用。
我们看几道经典题目:
引例
一棵仙人掌,每条边有边权,求 1 号节点到每个节点的最短路径长度。节点个数 n≤105n≤105
我们先 dfs 一遍的到 dfs 树,这是我们之后处理的基础。
然后从一号节点开始 dp 。假设现在已经求出 1 到 u 的距离,枚举 u 的每一个儿子,如果该儿子不在环内,加上边权继续;否则,枚举换上的我们暴力求环上的每个点到 1 的距离,然后在分别从环上的每个点继续 dp 。
[BZOJ1023]cactus仙人掌图[SHOI2008]
一棵带边权仙人掌,求直径(最短路径最长的两个点的最短路径长度)。节点个数
我们 DFS 可以得到一颗 DFS 树。这里的 DFS 与 Tarjan 比较类似(大概就是 Tarjan),对于现在访问的节点 u 我们记录下来 low[u],dfn[u],dpt[u],fa[u] (前两个含义参考 Tarjan ,dpt 是深度(deepth) , fa 是 u 在 DFS 树上的父亲),对于没访问的(即 dfn 为 0 的)节点继续递归,向上回溯时更新 low 值。
遍历每条边时如果 low[v] 大于 dfn[u] ,说明此边为树边;对于一个节点,若其某个儿子在 DFS 树上的父亲不是它(没错,它的儿子的父亲不是它,机房的小伙伴们都笑疯了),那么说明这里出现了一个环,且此节点为环的根(深度最小的节点),它的那个儿子为环的尾(环中最后被遍历到的,也就是 DFS 树的叶子节点)
我们做以上这些的目的主要就是判断环和桥,然后分别 DP 处理。
我们定义f[i] 为以 i 为端点的最长链的长度。
对于桥来说十分简单(此时假设没有环):
这里我们在代码中的写法稍有不同:
先更新 ans ,后更新 f[u]。
遍历所有儿子,对每个儿子,f[u] 中存的可能是(我们只考虑是的情况,因为只有此时才对结果有影响,这也是为什么上一条要确保) second_max(f[v]) ,而 f[v] 可能是(依旧只考虑是的情况) max(f[v]),所以我们只需用 f[u]+f[v]+1 来更新 ans 即可;
现在我们考虑环的问题:
对于一个环,我们的宗旨是把它缩成一个点(即把一个环的 f 信息都存在其根上),然后就可以开心地按之前的方式 DP 啦!
由于环中更新答案的时候只转一圈不能保证答案最优(因为有可能最优的那一部分环被根分开了),又由于环中距离的定义是最短路,所以我们只要转够一圈半即可。
如图:
定义环的节点集合为 C ,记环中一点 a 在环中的遍历顺序为 aid 、环长(环中节点个数)为 L ,则在环中两个点之间的距离
我们记一个环的根为 x 、尾为 y。
实现的时候,我们把环中的节点的 f 依次存在一个数组里,然后将这个数组倍长(就是将其复制一遍放到尾部),遍历时动一定一(这里莫名怀念小晓笑潇),若之间相差大于 L2 就 continue,再用单调队列优化一下, ans 就能轻松更新好了。之后再按照公式更新 f[x] 即可。
整个算法过程的时间复杂度仅为 O(N),还是蛮快的。
#include
#include
using namespace std;
const int MAXN=5e4+5,MAXM=MAXN<>
int n,m;
struct E{int next,to;} e[MAXM<1];int>
void addEdge(int u,int v)
{
e[++ecnt]=(E){G[u],v};G[u]=ecnt;
e[++ecnt]=(E){G[v],u};G[v]=ecnt;
}
int ans;
int f[MAXN];
int dfn[MAXN],dcnt,low[MAXN],dpt[MAXN],fa[MAXN];
int que[MAXN<><>
void solve(int x,int y)
{
int cnt=dpt[y]-dpt[x]+1,head=1,tail=1,i;
for(i=y;i!=x;i=fa[i]) a[cnt--]=f[i];a[1]=f[x];
cnt=dpt[y]-dpt[x]+1;
for(i=1;i<=cnt;i++) a[i+cnt]="">
que[1]=1;
for(i=2;i<=cnt+(cnt>>1);i++)
{
if(i-que[head]>(cnt>>1)) head++;
ans=max(ans,a[i]+i+a[que[head]]-que[head]);
while(head<=tail&&a[i]-i>=a[que[tail]]-que[tail]) tail--;
que[++tail]=i;
}
for(i=2;i<=cnt;i++) f[x]="">
}
void dfs(int u)
{
int i;
dfn[u]=low[u]=++dcnt;
for(i=G[u];i;i=e[i].next)
{
int v=e[i].to;
if(v==fa[u]) continue;
if(!dfn[v]) {fa[v]=u;dpt[v]=dpt[u]+1;dfs(v);}
low[u]=min(low[u],low[v]);
if(low[v]>dfn[u]) ans=max(ans,f[u]+f[v]+1),f[u]=max(f[u],f[v]+1);
//对树边的更新
}
for(i=G[u];i;i=e[i].next)
{
int v=e[i].to;
if(fa[v]!=u&&dfn[u]
solve(u,v);
}
}
int main()
{
int i,j;
scanf('%d%d',&n,&m);
for(i=1;i<>
{
int k;scanf('%d',&k);
int u,v;scanf('%d',&u);
for(j=2;j<=k;j++) scanf('%d',&v),addedge(u,v),u="">
}
dfs(1);printf('%d\n',ans);
return 0;
}
圆方树
通过前面的几道例题,我们发现:其实解决仙人掌 DP 问题不过就是参照树上的解法然后对环上的情况特殊处理一下,把环的信息记录到一个点上。
其实,神犇们很早就发现了这一点,于是他们想:既然仙人掌的许多问题在树上都有现成的解法,那么如果直接把仙人掌变成树,岂不美哉?
于是,神犇们成功的仙人掌变成树,并给这种树起了一个生动形象的名字:圆方树,它能解决大多数静态仙人掌问题
定义
构造
从任意一个点跑 Tarjan 求点双连通分量;
对于每个点双,从栈中取出,这时栈中的顺序就是环上的顺序,在圆方树中建立方点,依次向栈中的圆点连边;
如果这条边是桥边,我们直接在圆方树中加入它。
性质
两个方点不会相连
圆方树是无根树
子仙人掌:以 r 为根的仙人掌上的点 p 的子仙人掌是从仙人掌中起吊 p 到 r 的简单路径上的所有边后, p 所在的连通块。
以 r 为根的仙人掌中点 p 的子仙人掌就是圆方树以 r 为根时点 p 的子树中的所有圆点。
[BZOJ4316] 小 C 的独立集
在一个无向连通图中选出若干个点,这些点互相没有边连接,并使取出的点尽量多。数据保证图的一条边属于且仅属于一个简单环,图中没有重边和自环。点数
在一个无向连通图中选出若干个点,这些点互相没有边连接,并使取出的点尽量多。数据保证图的一条边属于且仅属于一个简单环,图中没有重边和自环。点数
n
≤
1
0
6
n\leq10^6
n≤106 ,边数
m
≤
1
0
6
m\leq10^6
m≤106
类比树上的解法:设f[i][0/1] 表示点 i 是否选时子树内的最大独立集;
如果一条边连接两个圆点,用树上转移方式即可;
而对于连接圆点和方点的情况,把这个换中所有点拿出来,跑一个环上的 DP 。
时间复杂度: O(n)
其实我们在解这道题的时候,没有必要真正建出圆方树,我在代码里建出圆方树只是为了举例说明,是为让大家熟悉圆方树的建法。
#include
#include
#include
const int MAXN=2e5+5,MAXM=2e5+5,INF=~0U>>1;
int n,m,newn;//newn:圆方树的点数
struct CFS
{
struct E{int next,to;} e[MAXM];int ecnt,G[MAXN];
void addEdge(int u,int v){e[++ecnt]=(E){G[u],v};G[u]=ecnt;}
void addEdge2(int u,int v){addEdge(u,v);addEdge(v,u);}
CFS(){ecnt=1;}
} G,T;
int f[MAXN][2],g[MAXN][2],gcnt;
void treeDP(int u,int from)
{
int i;
if(u<>
{
f[u][0]=0;f[u][1]=1;
for(i=T.G[u];i;i=T.e[i].next)
{
int v=T.e[i].to;
if(v==from) continue;
treeDP(v,u);
if(v>n) continue;
f[u][0]+=std::max(f[v][0],f[v][1]);
f[u][1]+=f[v][0];
}
}
else
{
for(i=T.G[u];i;i=T.e[i].next)
if(T.e[i].to!=from)
treeDP(T.e[i].to,u);
gcnt=0;
for(i=T.G[u];i;i=T.e[i].next)
{
g[++gcnt][0]=f[T.e[i].to][0];
g[gcnt][1]=f[T.e[i].to][1];
}
for(i=gcnt-1;i;i--)
{
g[i][0]+=std::max(g[i+1][0],g[i+1][1]);
g[i][1]+=g[i+1][0];
}
f[from][0]=g[1][0];
gcnt=0;
for(i=T.G[u];i;i=T.e[i].next)
{
g[++gcnt][0]=f[T.e[i].to][0];
g[gcnt][1]=f[T.e[i].to][1];
}
g[gcnt][1]=-INF;
for(i=gcnt-1;i;i--)
{
g[i][0]+=std::max(g[i+1][0],g[i+1][1]);
g[i][1]+=g[i+1][0];
}
f[from][1]=g[1][1];
}
}
int fa[MAXN],dfn[MAXN],dcnt;
bool onRing[MAXN];
void dfs(int u,int la)
{
int i,j;dfn[u]=++dcnt;
for(i=G.G[u];i;i=G.e[i].next)
{
int v=G.e[i].to;
if(v==la) continue;
if(!dfn[v])
{
fa[v]=u;onRing[u]=false;
dfs(v,u);
if(!onRing[u]) T.addEdge2(u,v);
}
else
{
if(dfn[v]>dfn[u]) continue;
for(j=u,++newn;j!=fa[v];j=fa[j])
T.addEdge2(newn,j),onRing[j]=true;
}
}
}
int main()
{
int i,u,v;
scanf('%d%d',&n,&m);newn=n;
for(i=1;i<>
{
scanf('%d%d',&u,&v);
G.addEdge2(u,v);
}
dfs(1,0);treeDP(1,0);
printf('%d\n',std::max(f[1][0],f[1][1]));
}
来源:
http://www.360doc.com/content/19/0421/12/63590853_830310554.shtml