並查集擴充套件應用

legendcn發表於2024-07-09

並查集擴充套件應用

A. 貨物運輸

題目描述

\(n\) 座城市和 \(m\) 條雙向道路。已知走過每條邊所需要的汽油量,\(q\) 次詢問,求汽油量為 \(l\) 的車可以在多少對城市之間運送貨物。(汽車到達城市會立刻把油全部加滿)

題解

這道題沒有強制線上,所以可以考慮進行離線。

對於大小為 \(n\) 一個連通塊,兩兩相連的點對一共有 \(\frac{n(n-1)}{2}\)
於是 很難不想到 使用並查集維護連通塊的大小,動態更新答案。

# include <bits/stdc++.h> 
using namespace std; 
using namespace IOS; 
typedef long long ll; 
# define int long long 
# define lc u << 1
# define rc u << 1 | 1 
const int N = 100005; 

int n, m, q; 
struct edge 
{
    int u, v, w; 
    bool operator < (const edge &t) const { return w < t.w; }
} e[N]; 
struct query
{
    int l, id; 
    bool operator < (const query &t) const { return l < t.l; }
} Q[N]; 
int fa[N], sz[N]; 
int find_fa (int u) { return u == fa[u] ? u : fa[u] = find_fa (fa[u]); }
int ans; 
void union_fa (int u, int v)
{
    u = find_fa (u), v = find_fa (v); 
    if (u != v)
    {
        fa[v] = u; 
        ans -= sz[u] * (sz[u] - 1) + sz[v] * (sz[v] - 1); 
        sz[u] += sz[v]; 
        ans += sz[u] * (sz[u] - 1); 
	}
}
int res[N]; 
signed main ()
{
    read (n, m, q); 
    for (int i = 1; i <= m; i ++ ) read (e[i].u, e[i].v, e[i].w); 
    sort (e + 1, e + 1 + m); 
    for (int i = 1; i <= q; i ++ )
    {
        read (Q[i].l); 
        Q[i].id = i; 
    }
    sort (Q + 1, Q + 1 + q); 
	for (int i = 1; i <= n; i ++ ) fa[i] = i, sz[i] = 1; 
    for (int i = 1, j = 1; i <= q; i ++ )
    {
        while (j <= m && e[j].w <= Q[i].l) union_fa (e[j].u, e[j].v), j ++ ; 
        res[Q[i].id] = ans; 
	}
    for (int i = 1; i <= q; i ++ ) write (res[i] / 2, '\n'); 
	return 0;
}

B. 銀河英雄傳說

題目描述

yyc 在一次戰鬥中,他將 1233 戰場劃分成 \(30000\) 列,每列依次編號為 \(1, 2,\ldots ,30000\)。之後,他把自己的戰艦也依次編號為 \(1, 2, \ldots , 30000\),讓第 \(i\) 號戰艦處於第 \(i\) 列。這是初始陣形。當進犯之敵到達時,yyc 會多次釋出合併指令。合併指令為 M i j,含義為第 \(i\) 號戰艦所在的整個戰艦佇列,作為一個整體(頭在前尾在後)接至第 \(j\) 號戰艦所在的戰艦佇列的尾部。顯然戰艦佇列是由處於同一列的一個或多個戰艦組成的。合併指令的執行結果會使佇列增大。

然而,cdx 可以透過龐大的情報網路隨時監聽 yyc 的艦隊調動指令。

在 yyc 釋出指令調動艦隊的同時,cdx 為了及時瞭解當前 yyc 的戰艦分佈情況,也會發出一些詢問指令:C i j。該指令意思是,詢問電腦,yyc 的第 \(i\) 號戰艦與第 \(j\) 號戰艦當前是否在同一列中,如果在同一列中,那麼它們之間佈置有多少戰艦。

題解

\(fa_i\) 表示飛船 \(i\) 所在列的隊頭。
\(d_i\) 表示飛船 \(i\) 到其所在佇列隊頭的距離,則飛船 \(i\)和飛船 \(j\) 之間的飛船數量 \(|d_i-d_j|-1\)
\(sz_i\) 表示飛船 \(i\) 所在的佇列的大小

使用並查集維護即可。

# include <bits/stdc++.h> 
using namespace std; 
using namespace IOS; 
typedef long long ll; 
//# define int long long 
# define lc u << 1
# define rc u << 1 | 1 
const int N = 30005; 

int m; 
int fa[N], sz[N], d[N]; 
int find_fa (int u)
{
    if (u == fa[u]) return fa[u]; 
    int v = find_fa (fa[u]); 
    d[u] += d[fa[u]]; 
    fa[u] = v; 
    return v; 
}
void union_fa (int u, int v)
{
	u = find_fa (u), v = find_fa (v);
	if (u != v) fa[u] = v, d[u] = sz[v], sz[v] += sz[u];
}
signed main ()
{
	for (int i = 1; i < N; i ++ ) fa[i] = i, sz[i] = 1;
	read (m); 
	while (m -- )
	{
		char op; int u, v; read (op, u, v); 
		if (op == 'M') union_fa (u, v);
		else
		{
			int fu = find_fa (u), fv = find_fa (v);
            if (fu != fv) write (-1, '\n'); 
            else write (max (0, abs (d[u] - d[v]) - 1), '\n');
		}
	}
	return 0;
}

C. 羅馬遊戲

題目描述

監獄裡面有 \(n\) 個人,每個人都是一個獨立的團。最近統計了每個人善良程度。獄卒可以發兩種命令:

  1. Merge(i, j)。把 \(i\) 所在的團和j所在的團合併成一個團。如果 \(i, j\) 有一個人是死人,那麼就忽略該命令。
  2. Kill(i)。把 \(i\)所在的團裡面善良程度最低的人殺死。如果 \(i\) 這個人已經死了,這條命令就忽略。 獄卒希望他每釋出一條 kill 命令,下面的刀斧手就把被殺的人的善良程度報上來。(如果這條命令被忽略,那麼就報 \(0\)

題解

啟發式合併+並查集

# include <bits/stdc++.h> 
using namespace std; 
using namespace IOS; 
typedef long long ll; 
//# define int long long 
# define lc u << 1
# define rc u << 1 | 1 
const int N = 1000005; 

int n, q; 
bool dead[N]; 
int fa[N], sz[N]; 
priority_queue <int, vector <int>, greater <int> > grp[N]; 
int find_fa (int u) { return u == fa[u] ? u : fa[u] = find_fa (fa[u]); }
void union_fa (int u, int v)
{
    u = find_fa (u), v = find_fa (v); 
    if (u != v)
    {
        if (sz[u] < sz[v]) swap (u, v); 
        fa[v] = u; 
        while (!grp[v].empty ()) grp[u].push (grp[v].top ()), grp[v].pop (); 
        sz[u] += sz[v]; 
    }
}
map <int, int> mp; 
signed main ()
{
    read (n); 
    for (int i = 1; i <= n; i ++ ) fa[i] = i, sz[i] = 1; 
    for (int i = 1; i <= n; i ++ )
    {
        int x; read (x); 
        grp[i].push (x); 
        mp[x] = i; 
    }
    read (q); 
    while (q -- )
    {
        char c; int x, y; read (c, x); 
        if (c == 'M')
        {
            read (y); 
            if (dead[x] || dead[y]) continue; 
            union_fa (x, y); 
        }
        else
        {
            if (dead[x]) { write (0, '\n'); continue; }
            x = find_fa (x); 
            int u = grp[x].top (); grp[x].pop (); 
            dead[mp[u]] = 1; 
            write (u, '\n'); 
        }
    }
    return 0; 
}

D. 矩陣

題目描述

有一個 \(n\times m\)的矩陣,初始每個格子的權值都為 \(0\),可以對矩陣執行兩種操作:

  1. 選擇一行,該行每個格子的權值加 \(1\) 或減 \(1\)
  2. 選擇一列,該列每個格子的權值加 \(1\) 或減\(1\)

現在有 \(K\) 個限制,每個限制為一個三元組 \((x,y,c)\),代表格子 \((x,y)\) 權值等於\(c\)
問是否存在一個操作序列,使得操作完後的矩陣滿足所有的限制。
如果存在輸出 Yes,否則輸出 No

題解

定義橫著是加,豎著是減。那麼每個限制可以轉化為:\(A_x-A_y=c\)\(A\) 增加量或減少量)
帶權並查集維護的是到根節點的距離,那麼這個也可以類比到根節點的距離。
\(A_x-A_y=c\) 轉化為 \(A_x=A_y+c\)
那麼每次修改的其實就是到根節點的距離。

# include <bits/stdc++.h> 
using namespace std; 
using namespace IOS; 
typedef long long ll; 
//# define int long long 
# define lc u << 1
# define rc u << 1 | 1 
const int N = 2005; 

int n, m, q; 
int fa[N], d[N]; 
int find_fa (int u)
{
    if (u == fa[u]) return u; 
    int v = find_fa (fa[u]); 
    d[u] += d[fa[u]]; 
    return fa[u] = v; 
}
signed main ()
{
    int T; read (T); 
    while (T -- )
    {
        read (n, m, q); 
        for (int i = 1; i <= n + m; i ++ ) fa[i] = i, d[i] = 0; 
        int ans = 1; 
        while (q -- )
        {
            int u, v, w; read (u, v, w); 
            v += n; 
            int fu = find_fa (u), fv = find_fa (v); 
            if (fu != fv)
            {
                fa[fu] = fv; 
                d[fu] = d[v] - d[u] + w; 
            }
            else if (d[u] - d[v] != w) ans = 0; 
		}
		if (ans) puts("Yes");
		else puts("No");
	}
	return 0;
}

相關文章