图论

最短路

floyd

适用于任何图,不管有向无向,边权正负,但是最短路必须存在。(不能有负环)

code $(n^3)$
初始化:
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= n; j ++ )
            if (i == j) d[i][j] = 0;
            else d[i][j] = INF;

// 算法结束后,d[a][b]表示a到b的最短距离
void floyd()
{
    for (int k = 1; k <= n; k ++ )
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= n; j ++ )
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

spfa(队列优化的Bellman-Ford算法)

可以求出有负权的图的最短路,并可以对最短路不存在的情况进行判断。

求最短路

code$(km)$k:平均入队次数;最坏$(nm)$
int n;      // 总点数
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N];        // 存储每个点到1号点的最短距离
bool st[N];     // 存储每个点是否在队列中
// 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1
int spfa()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    queue<int> q;
    q.push(1);
    st[1] = true;

    while (q.size())
    {
        auto t = q.front();
        q.pop();

        st[t] = false;

        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                if (!st[j])     // 如果队列中已存在j,则不需要将j重复插入
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

判断负环

code
int n;      // 总点数
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N], cnt[N];        // dist[x]存储1号点到x的最短距离,cnt[x]存储1到x的最短路中经过的点数
bool st[N];     // 存储每个点是否在队列中
// 如果存在负环,则返回true,否则返回false。
bool spfa()
{
    // 不需要初始化dist数组
    // 原理:如果某条最短路径上有n个点(除了自己),那么加上自己之后一共有n+1个点,由抽屉原理一定有两个点相同,所以存在环。

    queue<int> q;
    for (int i = 1; i <= n; i ++ )
    {
        q.push(i);
        st[i] = true;
    }

    while (q.size())
    {
        int t = q.front();
        q.pop();

        st[t] = false;

        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;
                if (cnt[j] >= n) return true;       // 如果从1号点到x的最短路中包含至少n个点(不包括自己),则说明存在环
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    return false;
}

Dijkstra 算法

是一种求解 非负权图 上单源最短路径的算法

code ($n^2$暴力)
int g[N][N];  // 存储每条边
int dist[N];  // 存储1号点到每个点的最短距离
bool st[N];   // 存储每个点的最短路是否已经确定

// 求1号点到n号点的最短路,如果不存在则返回-1
int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    for (int i = 0; i < n - 1; i ++ )
    {
        int t = -1;     // 在还未确定最短路的点中,寻找距离最小的点
        for (int j = 1; j <= n; j ++ )
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;

        // 用t更新其他点的距离
        for (int j = 1; j <= n; j ++ )
            dist[j] = min(dist[j], dist[t] + g[t][j]);

        st[t] = true;
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}
code($mlog_m$优化)
typedef pair<int, int> PII;

int n;      // 点的数量
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N];        // 存储所有点到1号点的距离
bool st[N];     // 存储每个点的最短距离是否已确定

// 求1号点到n号点的最短距离,如果不存在,则返回-1
int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    priority_queue<PII, vector<PII>, greater<PII>> heap;
    heap.push({0, 1});      // first存储距离,second存储节点编号

    while (heap.size())
    {
        auto t = heap.top();
        heap.pop();

        int ver = t.second, distance = t.first;

        if (st[ver]) continue;
        st[ver] = true;

        for (int i = h[ver]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > distance + w[i])
            {
                dist[j] = distance + w[i];
                heap.push({dist[j], j});
            }
        }
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

树的直径

树上任意两节点之间最长的简单路径即为「树的直径」

求法1 两次DFS

定理:在一棵树上,从任意节点 开始进行一次 DFS,到达的距离其最远的节点 必为直径的一端。(所有路径均不为负)

!!若存在负权边,无法使用两次 DFS 的方式求解直径。
如果需要求出一条直径上所有的节点,则可以在第二次 DFS 的过程中,记录每个点的前序节点,即可从直径的一端一路向前,遍历直径上所有的节点。

code
const int N = 3e5 + 10, M = N << 1;

int n;
int h[N], e[M], w[M], ne[M], idx;
int dia[N], rt;
int dist[N];

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

void dfs(int u, int fa, int flag)
{
    for (int i = h[u]; ~i; i = ne[i])
    {
        int v = e[i];
        if (v == fa) continue;
        dist[v] = dist[u] + w[i];
        if (dist[v] > dist[rt]) rt = v;
        if (flag) dia[v] = u;//第二次DFS,记录前序节点
        dfs(v, u, flag);
    }
}

void diameter()
{
    dfs(1, 0, 0);
    dist[rt] = 0;
    dfs(rt, 0, 1);
}

int main()
{
    memset(h, -1, sizeof h);
    rd(n);
    for (int i = 1, u, v, c; i < n; i ++ )
    {
        rd(u), rd(v), rd(c);
        add(u, v, c);
        add(v, u, c);
    }
    diameter();
    printf("%d\n", dist[rt]);
    for (int i = rt; i; i = dia[i])
    printf("%d ", i);
    return 0;
}

求法2 树形DP

树形 DP 可以在存在负权边的情况下求解出树的直径。

如果需要求出一条直径上所有的节点,则可以在 DP 的过程中,记录下每个节点能向下延伸的最远距离与次远距离所对应的子节点,之后再找到对应的 \(u\) ,使得 \(d=d_1u+d_2u\) ,即可分别沿着从 \(u\) 开始的最远距离和次远距离对应的子节点向上遍历直径上所有的节点。

code
const int N = 3e5 + 10, M = N << 1;

int n;
int h[N], e[M], w[M], ne[M], idx;
int zd[N], cd[N];
int zp[N], cp[N], fa[N], rt;
int dist;

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

void diameter(int u, int father)
{
    zd[u] = cd[u] = 0;  
    fa[u] = father;
    zp[u] = cp[u] = u;
    for (int i = h[u]; ~i; i = ne[i])
    {
        int v = e[i];
        if (v == father) continue;
        diameter(v, u);
        int t = zd[v] + w[i];
        if (t > zd[u])
        {
            cd[u] = zd[u];
            cp[u] = zp[u];
            zd[u] = t;
            zp[u] = zp[v];
        }
        else if (t > cd[u])
        {
            cd[u] = t;
            cp[u] = zp[v];
        }
    }
    if (zd[u] + cd[u] > dist)
    {
        rt = u;
        dist = zd[u] + cd[u];
    }
}

int main()
{
    memset(h, -1, sizeof h);
    rd(n);
    for (int i = 1, u, v, c; i < n; i ++ )
    {
        rd(u), rd(v), rd(c);
        add(u, v, c);
        add(v, u, c);
    }
    diameter(1, 0);
    printf("%d\n", dist);
    for (int i = zp[rt]; i != fa[rt]; i = fa[i])
    printf("%d ", i);
    for (int i = cp[rt]; i != rt; i = fa[i])
    printf("%d ",i);
    return 0;
}

最近公共祖先

性质

  1. \(\text{LCA}(\{u\})=u\)
  2. \(u\)\(v\) 的祖先,当且仅当 \(\text{LCA}(u,v)=u\)
  3. 如果 \(u\) 不为 \(v\) 的祖先并且 \(v\) 不为 \(u\) 的祖先,那么 \(u\),\(v\) 分别处于 \(\text{LCA}(u,v)\) 的两棵不同子树中;
  4. 前序遍历中,\(\text{LCA}(S)\) 出现在所有 \(S\) 中元素之前,后序遍历中 \(\text{LCA}(S)\) 则出现在所有 \(S\) 中元素之后;
  5. 两点集并的最近公共祖先为两点集分别的最近公共祖先的最近公共祖先,即 \(\text{LCA}(A\cup B)=\text{LCA}(\text{LCA}(A)\), \(\text{LCA}(B))\)
  6. 两点的最近公共祖先必定处在树上两点间的最短路上;
  7. \(d(u,v)=h(u)+h(v)-2h(\text{LCA}(u,v))\),其中 \(d\) 是树上两点间的距离,\(h\) 代表某点到树根的距离。

求法1 倍增法

code(bfs)
const int N = 5e4 + 10, M = N << 1;

int n, m;
int h[N], e[M], ne[M], w[M], idx;
int depth[N], fa[N][17];
int q[N];

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void bfs(int root)
{
    memset(depth, 0x3f, sizeof depth);
    depth[0] = 0, depth[root] = 1;
    int hh = 0, tt = 0;
    q[0] = root;
    while (hh <= tt)
    {
        int t = q[hh ++ ];
        for (int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            if (depth[j] > depth[t] + 1)
            {
                depth[j] = depth[t] + 1;
                q[ ++ tt] = j;
                fa[j][0] = t;
                for (int k = 1; k <= 16; k ++ )
                    fa[j][k] = fa[fa[j][k - 1]][k - 1];
            }
        }
    }
}

int lca(int a, int b)
{
    if (depth[a] < depth[b])
        swap(a, b);
    for (int k = 16; k >= 0; k -- )
        if (depth[fa[a][k]] >= depth[b])
            a = fa[a][k];
    if (a == b)
        return a;
    for (int k = 16; k >= 0; k -- )
        if (fa[a][k] != fa[b][k])
        {
            a = fa[a][k];
            b = fa[b][k];
        }
    return fa[a][0];
}
code(dfs)
void dfs(int u, int father)
{
    depth[u] = depth[fa[u][0] = father] + 1;
    for (int k = 1; 1 << k < depth[u]; k ++ )
        fa[u][k] = fa[fa[u][k - 1]][k - 1];
    for (int i = h[u]; ~i; i = ne[i])
    {
        int v = e[i];
        if (v != father)
        	dfs(v, u);
    }
}

int lca(int a, int b)
{
    if (depth[a] < depth[b])
        swap(a, b);
    for (int k = 17; k >= 0; k -- )
        if (depth[fa[a][k]] >= depth[b])
            a = fa[a][k];
    if (a == b)
        return a;
    for (int k = 17; k >= 0; k -- )
        if (fa[a][k] != fa[b][k])
        {
            a = fa[a][k];
            b = fa[b][k];
        }
    return fa[a][0];
}

差分约束

定义

差分约束系统 是一种特殊的 \(n\) 元一次不等式组,它包含 \(n\) 个变量 \(x_1,x_2,\dots,x_n\) 以及 \(m\) 个约束条件,每个约束条件是由两个其中的变量做差构成的,形如 \(x_i-x_j\leq c_k\)(\(c_k\)为常数)。要解决的问题是:求一组解 \(x_1=a_1,x_2=a_2,\dots,x_n=a_n\),使得所有的约束条件得到满足,否则判断出无解。

过程

把每个变量 \(x_i\) 看做图中的一个结点,对于每个约束条件 \(x_i-x_j\leq c_k\),从结点 \(j\) 向结点 \(i\) 连一条长度为 \(c_k\) 的有向边。
\(dist[0]=0\) 并向每一个点连一条权重为 \(0\) 边,跑单源最短路,若图中存在负环,则给定的差分约束系统无解,否则,\(x_i=dist[i]\) 为该差分约束系统的一组解。

注意

如果 \(\{a_1,a_2,\dots,a_n\}\) 是该差分约束系统的一组解,那么对于任意的常数 \(d\)\(\{a_1+d,a_2+d,\dots,a_n+d\}\) 显然也是该差分约束系统的一组解,因为这样做差后 \(d\) 刚好被消掉。

posted @ 2022-09-24 11:01  kroyosh  阅读(47)  评论(0)    收藏  举报