1. 程式人生 > 實用技巧 >ZOJ - 3640 Help Me Escape 概率DP

ZOJ - 3640 Help Me Escape 概率DP

目錄

LCA

1. 演算法分析

1.1 求LCA的四種方法

1.樹上倍增法:
    倍增思想:\(f[i][j]\)表示i這個位置向上走2^j步後到達x,則有狀態轉移:\(f[y][j] = f[[y][j-1]][j-1]\),利用這個不斷處理出f陣列,樹上倍增法能夠得到 \(f[x][i]\) 陣列和 \(d[i]\) 陣列,利用這兩個陣列可以求出很多的東西。這是線上做法
2.tarjan演算法:
    dfs的特性和並查集的特性。把所有點分成三類,第一類:正在搜尋的點,第二類:已經回溯完的點,第三類:還沒有搜尋過的點,每次搜尋的時候,記當前點為x,把點x做個標記,然後判斷和這個點對應的點y是否已經回溯過了,如果y已經回溯過了,那麼x和y的lca即為y的get(y)得到的節點。這是離線

做法
3.dfs+ST
4.樹剖求lca

1.2 求lca的兩種場景

  1. 求任意兩個點的lca
  2. 求集合的lca:求一個集合的lca就是求這個集合中dfs序最小的點和dfs序最大的點的lca

2. 板子

2.1 樹上倍增法

HDU 2586

#include <bits/stdc++.h>

using namespace std;

const int N = 2e5 + 10;
int f[N][20], d[N], dist[N];  // f[i][j]表示從i開始,往上走2^j步到達的點,d為深度,dist為距離
int e[N], ne[N], h[N], idx, w[N];
int T, n, m, t;  // t為數的深度
queue<int> q;

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

// 預處理:得到每個點的深度,距離,f陣列
void bfs()
{
    q.push(1);  // 把根放入佇列,注意這裡有可能根不是1
    d[1] = 1;
    while (q.size())
    {
        int x = q.front();
        q.pop();
        for (int i = h[x]; i != -1; i = ne[i])
        {
            int y = e[i];
            if (d[y]) continue;
            d[y] = d[x] + 1;  // 更新深度
            dist[y] = dist[x] + w[i];  // 更新距離
            
            // 進行dp更新
            f[y][0] = x;
            for (int j = 1; j <= t; ++j)
            {
                f[y][j] = f[f[y][j - 1]][j - 1];  // 分兩段處理
            }
            q.push(y);
        }
    }
}

// 查詢x和y的最近公共祖先
int lca(int x, int y)
{
    if (d[x] > d[y]) swap(x, y);  // 保證x的深度淺一點
    for (int i = t; i >= 0; --i)
        if (d[f[y][i]] >= d[x]) y = f[y][i];  // 讓x和y到同一個深度
    if (x == y) return x;
    for (int i = t; i >= 0; --i)  // 讓x和y之差一步就能相遇
    {
        if (f[x][i] != f[y][i]) x = f[x][i], y = f[y][i];
    }
    return f[x][0];
}

int main()
{
     cin >> T;
     while (T--)
     {
         memset(h, -1, sizeof h);
         idx = t = 0;
         cin >> n >> m;
         t = (int)(log(n) / log(2)) + 1; // 得到樹的深度
         
         // 讀入一棵樹
         for (int i = 0; i < n - 1; ++i)
         {
             int a, b, c;
             scanf("%d %d %d", &a, &b, &c);
             add(a, b, c), add(b, a, c);
         }
        
        bfs();
        // 回答詢問
        for (int i = 1; i <= m; ++i)
        {
            int a, b;
            scanf("%d %d", &a, &b);
            printf("%d\n", dist[a] + dist[b] - 2 * dist[lca(a, b)]);
        }
     }
    return 0;
}

2.2 tarjan演算法

HDU 2586

#include <bits/stdc++.h>

using namespace std;

int const N = 1e5 + 10;
int e[N], ne[N], idx, ans[N], v[N], fa[N], d[N], h[N], w[N];
vector<int> query[N], query_id[N];
int t, n, m;

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

// 並查集查詢+路徑壓縮
int get(int x)
{
    if (fa[x] != x) fa[x] = get(fa[x]);
    return fa[x];
}

// tarjan演算法求lca
void tarjan(int x)
{
    // 記錄這個點走過一次,但是還沒有回溯
    v[x] = 1;
    
    // 遍歷每一個和x相連的點
    for (int i = h[x]; i != -1; i = ne[i])
    {
        int y = e[i];
        if (v[y]) continue;  // 這個點走過的話,不進行後面的操作
        d[y] = d[x] + w[i];
        tarjan(y);  // 得到y為根節點的所有子樹的d
        fa[y] = x;  // 更新y的父節點
    }
    
    // 判斷和x有關的lca詢問
    for (int i = 0; i < query[x].size(); ++i)
    {
        int y = query[x][i];
        int id = query_id[x][i];
        if (v[y] == 2)
        {
            int lca = get(y);  // 獲得lca:如果y點回溯,那麼lca為get(y)
            ans[id] = min(ans[id], d[x] + d[y] - 2 * d[lca]);  // 更新答案
        }
    }
    v[x] = 2;  // 標記x點回溯
}

int main()
{
    cin >> t;
    while (t--)
    {
        cin >> n >> m;
        
        // 初始化
        memset(h, -1, sizeof h);
        idx = 0;
        for (int i = 1; i <= n; ++i)
        {
            fa[i] = i;
            query[i].clear(), query_id[i].clear();
        }
        
        // 讀入樹邊
        for (int i = 1; i < n; ++i)
        {
            int a, b, c;
            scanf("%d %d %d", &a, &b, &c);
            add(a , b, c), add(b, a, c);
         }
         
        // 讀入詢問的邊
        for (int i = 1; i <= m; ++i)
        {
            int a, b;
            scanf("%d %d", &a, &b);
            if (a == b)
            {
                ans[i] = 0;
            }
            else
            {
                query[a].push_back(b), query[b].push_back(a);
                query_id[a].push_back(i), query_id[b].push_back(i);
                ans[i] = (1 << 30);
            }
        }
        
        // 做dfs求lca
        tarjan(1);
        
        // 輸出答案
        for (int i = 1 ; i <= m; ++i)
        {
            cout << ans[i] << endl;
        }
    }
    return 0;
}

2.3 dfs+ST

/*
我們需要維護陣列oula[i]= j表示j的dfs序為i,pos[i]=j表示i第一次在dfs序中出現的位置是j
len記錄dfs序的長度,dp[i][j]表示從i點出發走2^j步的範圍內最小的深度的點的座標,de[i]=j表示i的深度為j

本演算法為dfs+st表求lca,預處理時間O(nlogn), 查詢O(1)

演算法步驟:
1. dfs:預處理出dfs序
2. ST:預處理dp陣列
3. 給定任意兩個點:通過dp陣列得出這兩個點間的深度最小的點,即為lca
*/
#include <bits/stdc++.h>

using namespace std;

int const N = 1e5 + 1, M = N * 2;
int e[M], ne[M], h[N], idx;
int t, n, m;
int oula[M], len, pos[N], dp[M][23], de[M];
typedef pair<int, int> PII;
set<PII> s;

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

// 求dfs序
void dfs(int u, int fa, int d)
{
    oula[++len] = u;  // 記錄第len個為u點
    pos[u] = len;  // 記錄u點的dfs序為len(只記錄u點第一次出現的dfs序即可)
    de[len] = d;  // 記錄深度
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (j == fa) continue;

        dfs(j, u, d + 1);
        oula[++len] = u;  // 回溯時還要記錄
        de[len] = d;  
    }
}

// 得到x和y中深度比較小的那個
int Min(int x, int y)
{
    return de[x] > de[y]? y: x;
}

// 處理dp陣列
void ST()
{
    for (int i = 1; i <= len; ++i) dp[i][0] = i;

    for (int j = 1; (1 << j) <= len; ++j )
        for (int i = 1; i + (1 << j) - 1 <= len; ++i )
            dp[i][j] = Min(dp[i][j - 1], dp[i + (1 << (j - 1))][j - 1]);  // dp[i][j]是dp[i][j - 1]和dp[i + (1 << (j - 1))][j - 1]中深度更小的那個點的下標
}

// 求lca
int lca(int x, int y)
{
    // int x = pos[x], y = pos[y];  // 得到x和y點的dfs序的下標
    if (x > y) swap(x, y);
    int k = log2(y - x + 1);  // 計算在dfs序列中y和x的距離
    return Min(dp[x][k], dp[y - (1 << k) + 1][k]);  // lca為x~y範圍內深度最小的那個dfs序的下標
}

int main()
{
    cin >> t;
    while (t--)
    {
        scanf("%d", &n);
        s.clear();
        memset(h, -1, sizeof h);
        idx = 0, len = 0;
        memset(dp, 0, sizeof dp);
        memset(oula, 0, sizeof oula);
        memset(de, 0, sizeof de);
        memset(pos, 0, sizeof pos);
        for (int i = 1; i <= n - 1; ++i)
        {
            int a, b;
            scanf("%d %d", &a, &b);
            add(a, b);
            add(b, a);
        }
        
        int root = 0;
        dfs(root, 0, 1);  // dfs得出dfs序
        ST();  // 得出dp陣列

        scanf("%d", &m);
        while (m--)
        {
            getchar();
            char op = getchar();
            int num;
            scanf("%d", &num);
            if (op == '+') s.insert({pos[num], num});  // 插入
            else   // 刪除
            {
                auto it = s.lower_bound({pos[num], num});
                s.erase(it);
            }  

            // 輸出答案
            if (s.size() == 1) printf("%d\n", (*s.begin()).second);
            else if (s.size() >= 2)
            {
                auto left = (*s.begin()).first;
                auto right = (*prev(s.end())).first;
                printf("%d\n", oula[lca(left, right)]);
            }
            else printf("-1\n");
        }
    }
    return 0;
}

2.4 樹剖求lca

樹鏈剖分.md

3. 典型例題

acwing356 次小生成樹
題意: 給定一張 N 個點 M 條邊的無向圖,求無向圖的嚴格次小生成樹。設最小生成樹的邊權之和為sum,嚴格次小生成樹就是指邊權之和大於sum的生成樹中最小的一個。\(N≤10^5,M≤3*10^5\)
題解: 本題的思路是在求出最小生成樹的基礎上,找出一條非樹邊a->b,然後再樹上找出a->b的最大值,刪除這個最大值,加上非樹邊。
基於這個思路,目標就是要找出這個a->b在樹邊的最大值。
找出a->b在樹邊的最大值,可以先在樹上進行預處理,\(fa[i][j]\)表示i向上走\(2^j\)步到達的點,\(d1[i][j]\)表示i向上走\(2^j\)步範圍內的最大值,\(d2[i][j]\)表示i向上走\(2^j\)步範圍內的次大值,然後每次在找lca時順便找出,x到lca的最大值和次大值,y到lca的最大值和次大值,比較即可
程式碼:

#include<bits/stdc++.h>

using namespace std;

typedef long long LL;

const int N = 100010, M = 300010, INF = 0x3f3f3f3f;

int n, m;
struct Edge
{
    int a, b, w;
    bool used;
    bool operator< (const Edge &t) const
    {
        return w < t.w;
    }
}edge[M];
int p[N];
int h[N], e[M], w[M], ne[M], idx;
int depth[N], fa[N][17], d1[N][17], d2[N][17];
int q[N];

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

int find(int x)
{
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

// 找最小生成樹
LL kruskal()
{
    for (int i = 1; i <= n; i ++ ) p[i] = i;
    sort(edge, edge + m);
    LL res = 0;
    for (int i = 0; i < m; i ++ )
    {
        int a = find(edge[i].a), b = find(edge[i].b), w = edge[i].w;
        if (a != b)
        {
            p[a] = b;
            res += w;
            edge[i].used = true;
        }
    }

    return res;
}

// 建樹
void build()
{
    memset(h, -1, sizeof h);
    for (int i = 0; i < m; i ++ )
        if (edge[i].used)
        {
            int a = edge[i].a, b = edge[i].b, w = edge[i].w;
            add(a, b, w), add(b, a, w);
        }
}

// 預處理fa,d1,d2,depth
void bfs()
{
    memset(depth, 0x3f, sizeof depth);
    depth[0] = 0, depth[1] = 1;
    q[0] = 1;  // 把1當成根節點
    int hh = 0, tt = 0;
    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;  // 更新j的深度
                q[ ++ tt] = j;
                fa[j][0] = t;
                d1[j][0] = w[i], d2[j][0] = -INF;  // 求出d1和d2
                for (int k = 1; k <= 16; k ++ )
                {
                    int anc = fa[j][k - 1];
                    fa[j][k] = fa[anc][k - 1];
                    int distance[4] = {d1[j][k - 1], d2[j][k - 1], d1[anc][k - 1], d2[anc][k - 1]};
                    d1[j][k] = d2[j][k] = -INF;
                    for (int u = 0; u < 4; u ++ )
                    {
                        int d = distance[u];
                        if (d > d1[j][k]) d2[j][k] = d1[j][k], d1[j][k] = d;
                        else if (d != d1[j][k] && d > d2[j][k]) d2[j][k] = d;
                    }
                }
            }
        }
    }
}

// 找出a和b的lca,順便求出a到b之間的最大值和次大值
int lca(int a, int b, int w)
{
    static int distance[N * 2];
    int cnt = 0;
    if (depth[a] < depth[b]) swap(a, b);

    // 把a和b拉到同一個深度
    for (int k = 16; k >= 0; k -- )
        if (depth[fa[a][k]] >= depth[b])
        {
            distance[cnt ++ ] = d1[a][k];
            distance[cnt ++ ] = d2[a][k];
            a = fa[a][k];
        }

    // 把a和b之間的d1和d2的所有備選項求出來
    if (a != b)
    {
        for (int k = 16; k >= 0; k -- )
            if (fa[a][k] != fa[b][k])
            {
                distance[cnt ++ ] = d1[a][k];
                distance[cnt ++ ] = d2[a][k];
                distance[cnt ++ ] = d1[b][k];
                distance[cnt ++ ] = d2[b][k];
                a = fa[a][k], b = fa[b][k];
            }
        distance[cnt ++ ] = d1[a][0];
        distance[cnt ++ ] = d1[b][0];
    }

    // 把a和b之間的d1和d2求出來
    int dist1 = -INF, dist2 = -INF;
    for (int i = 0; i < cnt; i ++ )
    {
        int d = distance[i];
        if (d > dist1) dist2 = dist1, dist1 = d;
        else if (d != dist1 && d > dist2) dist2 = d;
    }

    if (w > dist1) return w - dist1;
    if (w > dist2) return w - dist2;
    return INF;
}

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 0; i < m; i ++ )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        edge[i] = {a, b, c};
    }

    LL sum = kruskal();  // 計算最小生成樹的值
    build();  // 把所有的樹邊建樹
    bfs();  // 預處理出d1,d2,fa,depth陣列

    LL res = 1e18;
    for (int i = 0; i < m; i ++ )
        if (!edge[i].used)  // 找出每條非樹邊,然後替換掉最大的那條樹邊
        {
            int a = edge[i].a, b = edge[i].b, w = edge[i].w;
            res = min(res, sum + lca(a, b, w));
        }
    printf("%lld\n", res);

    return 0;
}

Arab Collegiate Programming Contest 2015
題意: 求一個集合的lca
題解: 求一個集合的lca就是求這個集合中dfs序最小的點和dfs序最大的點的lca。dfs+ST處理。
程式碼:

#include <bits/stdc++.h>

using namespace std;

int const N = 1e5 + 1, M = N * 2;
int e[M], ne[M], h[N], idx;
int t, n, m;
int oula[M], len, pos[N], dp[M][23], de[M];
typedef pair<int, int> PII;
set<PII> s;

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

// 求dfs序
void dfs(int u, int fa, int d)
{
    oula[++len] = u;  // 記錄第len個為u點
    pos[u] = len;  // 記錄u點的dfs序為len(只記錄u點第一次出現的dfs序即可)
    de[len] = d;  // 記錄深度
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (j == fa) continue;

        dfs(j, u, d + 1);
        oula[++len] = u;  // 回溯時還要記錄
        de[len] = d;  
    }
}

// 得到x和y中深度比較小的那個
int Min(int x, int y)
{
    return de[x] > de[y]? y: x;
}

// 處理dp陣列
void ST()
{
    for (int i = 1; i <= len; ++i) dp[i][0] = i;

    for (int j = 1; (1 << j) <= len; ++j )
        for (int i = 1; i + (1 << j) - 1 <= len; ++i )
            dp[i][j] = Min(dp[i][j - 1], dp[i + (1 << (j - 1))][j - 1]);  // dp[i][j]是dp[i][j - 1]和dp[i + (1 << (j - 1))][j - 1]中深度更小的那個點的下標
}

// 求lca
int lca(int x, int y)
{
    // int x = pos[x], y = pos[y];  // 得到x和y點的dfs序的下標
    if (x > y) swap(x, y);
    int k = log2(y - x + 1);  // 計算在dfs序列中y和x的距離
    return Min(dp[x][k], dp[y - (1 << k) + 1][k]);  // lca為x~y範圍內深度最小的那個dfs序的下標
}

int main()
{
    cin >> t;
    while (t--)
    {
        scanf("%d", &n);
        s.clear();
        memset(h, -1, sizeof h);
        idx = 0, len = 0;
        memset(dp, 0, sizeof dp);
        memset(oula, 0, sizeof oula);
        memset(de, 0, sizeof de);
        memset(pos, 0, sizeof pos);
        for (int i = 1; i <= n - 1; ++i)
        {
            int a, b;
            scanf("%d %d", &a, &b);
            add(a, b);
            add(b, a);
        }
        
        int root = 0;
        dfs(root, 0, 1);  // dfs得出dfs序
        ST();  // 得出dp陣列

        scanf("%d", &m);
        while (m--)
        {
            getchar();
            char op = getchar();
            int num;
            scanf("%d", &num);
            if (op == '+') s.insert({pos[num], num});  // 插入
            else   // 刪除
            {
                auto it = s.lower_bound({pos[num], num});
                s.erase(it);
            }  

            // 輸出答案
            if (s.size() == 1) printf("%d\n", (*s.begin()).second);
            else if (s.size() >= 2)
            {
                auto left = (*s.begin()).first;
                auto right = (*prev(s.end())).first;
                printf("%d\n", oula[lca(left, right)]);
            }
            else printf("-1\n");
        }
    }
    return 0;
}