图论-怅望祁连(小知识点整理)

各种小知识点

Kruskal 重构树

Steiner 树

两道例题:

BZOJ3205 APIO2013 机器人
BZOJ2595 WC2008 游览计划

\(N\) 个点的连通无向图中至少包含给定的 \(K\) 个点的最小生成树。

\(S\) 表示连通状态, \(p\) 是当前状态中用来考虑转移的节点,可以理解成暂时的根之类的东西。

\[f(S,p)=min\{f(A,p)+f(B,p)\ |\ A \cup B = S\}\\f(S,p)=min\{f(S,q)+val(p)\} \]

第一个转移直接 DP ,第二个转移要求 \(p\)\(q\) 关联,用图论算法转移。它的正确性好像来自什么三角形不等式,不懂,不过确实可以感性理解。

注意 SPFA 可以加个 Small Label First 优化,开两个队列,每次取最小的来扩展,特殊情况下就是个 BFS 。

给自己留一份伪代码:


void spfa(int s)
{
    sort(Que_A);
    ...
    while () {
        p = min(Que_A.head, Que_B.head);
        for (...) {
            if (F[s][y] > F[s][x] + w) {
                ...
                Que_B.push();
            }
        }
    }
    return;
}

void steiner()
{
    for (int s = 0; s != (1 << K); ++s) {
        for (int t = (s - 1) & 1; t; t = (t - 1) & 1) {
            for (int p = 1; p <= N; ++p)
                F[s][p] = min(F[s][p], F[t][p] + F[s ^ t][p]);
        }
        for (int p = 1; p <= N; ++p)
            if (F[s][p] < INF)
                Que_A[++Q_cnt] = data(p, F[s][p]);
        spfa(s);
    }
    int ans = INF;
    for (int i = 1; i <= N; ++i)
        ans = min(ans, F[(1 << K) - 1][i]);
    return;
}

虚树

树很大,需要考虑的点很少,弄一颗虚树。树上路径求交的方法是把点集按 dfn 排序,然后相邻两个点找 lca 记录路径,这些路径是无公共边的。虚数也是先排序,然后用栈维护 dfn 递增的一条从根到当前讨论的点的链,最后在栈底的就是虚数的根。

void ins_node(int p)
{
    if (Top <= 1) {
        S[++Top] = p;
        return;
    }
    int lca = get_lca(p, S[Top]);
    if (lca == S[top]) {
        S[++Top] = p;
        return;
    }
    while (Top > 1 && Dfn[lca] <= Dfn[S[Top - 1]])
        ins_edge(S[Top - 1], S[Top]), --Top;
    if (lca != S[Top])
        ins_edge(lca, S[Top]), S[top] = lca;
    S[++Top] = p;
    return;
}

仙人掌 & 圆方树

小蒟蒻yyb的博客 - 仙人掌&圆方树学习笔记

仙人掌定义:一条边属于至多一个简单环。
桥边定义:非环边。
环边定义:非桥边。

首先,这个东西是处理无向图的。普通的圆方树只能处理仙人掌图,只对每个环建立方点,再向环上的点连边,其他点是圆点;广义的圆方树就是圆点和方点交替出现,对于桥边,强行在中间塞一个方点,然后还是对点双建立方点。

普通圆方树,一般套路是桥边直接转移,环上单独 DP :

void process_circle(int x, int y)
{
    for (int i = Dad[y]; i != x; i = Dad[i])
 		...F[i]...
    ...F[x]...
    return;
}
 
void dfs(int p, int d)
{
    Dad[p] = d;
    for (...) {
        if (!Dfn[t])
            ...
        else
            ...
        if (Low[t] > Dfn[p])
            F[p] = ...F[t]...
    }
    for (int i = Fst[p]; i; i = Nxt[i]) {
        if (Dad[t] != p && Dfn[t] > Dfn[p])
            process_circle(p, t);
    }
    return;
}

广义圆方树:

void dfs(int p)
{
    for (...) {// 遍历 p 能到达的点 t 
        if (!Dfn[t]) {
        	...
            if (Low[t] >= Dfn[p]) {
                ins_edge(p, ++Tot);// p 到方点
                int tmp;
                do
                    tmp = S[S_top--], ins_edge(tmp, Tot);// 方点到环上其他点
                while (tmp != t);// 注意 p 可能属于多个点双,所以弹栈到 t 为止
            }
        } else {
        	...
        }
    }
    return;
}

2-sat

边都是单向的,表示一种同类关系限制,即选了 a 就一定要选 b ,或者说 a 和 b 有相同的取值$。

注意不要让一些合法点的 Belong[i] == 0 ,全部都 dfs 一遍比较好。 Belong[i] == 0 会导致一些神奇的 Belong[i] == Belong[i'] 的情况,尽管此时实际上并没有产生矛盾。

输出任意方案时,对于对称图,可以根据反边边拓扑排序,然后每次讨论 \(i\) 时,如果值为 0 就把 \(i\) 标记为 1 ,对应点 \(i'\) 标记为 -1 ,如果值不为 0 就 continue 。可以证明,一组对应变量里选 SCC 编号较小者就是一组合理方案,考虑 Tarjan 搜索树 ++SCC 的操作即可。据说非对称图选 SCC 编号较小者也是正确的,而且此时前面拓扑排序的方法是错误的。所以好像选 SCC 编号较小者这种方法完全踩爆前一种方法?

posted @ 2019-01-17 22:24  derchg  阅读(192)  评论(0编辑  收藏  举报