最小生成樹

PassName發表於2024-03-07

最小生成樹

AcWing.346 走廊潑水節

簡要題意

給定一個 N 個節點的樹,要求增加若干條邊,把這棵樹擴充為完全圖,並滿足圖的唯一最小生成樹仍然是這棵樹。求增加的邊的權值總和最小是多少,保證邊權位非負整數。

題目分析

考慮 kruskal 的過程,是把權值從小到大排序,依次掃描每一個邊。那麼我們想讓這棵新的樹的最小生成樹仍然不變且是唯一的,那麼我們的邊權應該設為 \(z+1\)\(z\) 是當前掃描邊的邊長。那麼兩個點之間要比原圖多多少條邊呢?為 \(|S_x|*|S_y|-1\)\(S_x\) 表示 \(x\) 所在的並查集。所以只需要在原來跑 kruskal 的過程上多維護一個 \(S\) 就可以了。

struct rec
{
	int x, y, z;
	friend bool operator < (rec a, rec b)
	{
		return a.z < b.z;
	}
} edge[M];

int find(int x)
{
	return fa[x] == x ? x : fa[x] = find(fa[x]);
}

int kruscal()
{
	sort(edge + 1, edge + n);
	int idx = 0; ans = 0;
	for (rint i = 1; i <= n; i++) fa[i] = i, s[i] = 1;
	for (rint i = 1; i < n; i++)
	{
		int x = find(edge[i].x);
		int y = find(edge[i].y);
		if (x == y) continue;
		fa[x] = y;
		idx++;
		ans += (edge[i].z + 1) * (s[x] * s[y] - 1);
		s[y] += s[x];
	}
	if (idx < n - 1) return inf;
	return ans;
}

signed main()
{
	int T;
	cin >> T;
	while (T--)
	{
		cin >> n;
		for (rint i = 1; i < n; i++)
		{
			cin >> edge[i].x >> edge[i].y >> edge[i].z;
		}
		cout << kruscal() << endl;	
	}
	return 0;
}

AcWing 347. 野餐規劃

簡要題意

給定一張 \(N\) 個點 \(M\) 條邊的無向圖,求出無向圖的一棵最小生成樹,滿足 \(1\) 號節點的度數不超過給定的整數 \(S\)\(N\) 不超過 \(30\).

題目分析

首先,去掉一號節點之後,無向圖可能會分成若干個聯通塊。可以用深度優先遍歷劃分出圖中的每個聯通塊。設聯通塊共有 \(T\) 個,若 \(T > S\),則本題無解。

對於每個聯通塊,在這個聯通塊內部求出它的最小生成樹,然後從聯通塊中選出一個節點 \(p\)\(1\) 號節點相連,其中無向邊 \((1,p)\) 的權值儘量小。

此時,我們已經得到了原無向圖的一棵生成樹,\(1\) 號節點的度數為 \(T\)。我們還可以嘗試改動 \(S-T\),讓答案更優。

考慮無向圖中從節點 \(1\) 出發的每條邊 \((1,x)\) ,邊權為 \(z\)。如果 \((1,x)\) 還不在當前的生成樹中,那麼繼續找到當前生成樹中從 \(x\)\(1\) 的路徑上權值最大的邊 \((u,v)\),邊權為 \(w\)。求出使得 \(w-z\) 最大的點 \(x_0\)。若 \(x_0\) 對應的 \(w_0-z_0 > 0\),則從樹中刪掉邊 \((u_0,v_0)\),加入邊 \((1,x_0)\),答案就會變小 \(w_0-z_0\)

重複上一步 \(S - T\) 或者直到 \(w_0-z_0<=0\),就得到了題目所求的最小生成樹。

int n, s;
int tot;
int g[N][N];
int fa[N];
int block[N], cntb;
map<pair<int, int>, bool> v;
map<string, int> mp;
struct rec 
{
	int x, y, z;
    friend bool operator < (rec a, rec b)
	{
		return a.z < b.z;
	}
} edge[N];
struct node 
{
	int a, b;
	int dist;
} f[N];

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

int kruskal() 
{
	sort(edge + 1, edge + 1 + n);
	for (rint i = 1; i <= tot; i++) fa[i] = i;
	int ans = 0;
	for (rint i = 1; i <= n; i++) 
	{
		int x = find(edge[i].x);
		int y = find(edge[i].y);
		if (x == 1 || y == 1 || x == y) continue;
		fa[x] = y;
		v[{edge[i].x, edge[i].y}] = 1;
		v[{edge[i].y, edge[i].x}] = 1; 
		ans += edge[i].z;
	}
	return ans;
}

void dfs(int x) 
{
	for (rint y = 2; y <= tot; y++) 
	{
		if (!g[x][y] || block[y]) continue;
		block[y] = cntb;
		dfs(y);
	}
}

void dfs(int x, int father) 
{
	for (rint y = 2; y <= tot; y++) 
	{
		if (y == father || !v[{x, y}]) continue;
		if (~f[y].dist) continue;
		if (f[x].dist > g[x][y]) f[y] = f[x];
		else f[y] = {x, y, g[x][y]};
		dfs(y, x);
	}
}

signed main() 
{
	cin >> n;
	mp["Park"] = tot = 1;
	for (rint i = 1; i <= n; i++) 
	{
		string a, b;
		int c;
		cin >> a >> b >> c;
		if (!mp[a]) mp[a] = ++tot;
		if (!mp[b]) mp[b] = ++tot;
		g[mp[a]][mp[b]] = g[mp[b]][mp[a]] = c;
		edge[i] = {mp[a], mp[b], c};
	}
	cin >> s;
	for (rint i = 2; i <= tot; i++) 
	{
		if (!block[i]) 
		{
			cntb++;
			block[i] = cntb;
			dfs(i);
		}
	}
	int sum = kruskal();
	for (rint i = 1; i <= cntb; i++) 
	{
		int minn = inf, id = 0;
		for (rint j = 2; j <= tot; j++) 
		{
			if (block[j] == i) 
			{
				if (g[1][j] && minn > g[1][j]) 
				{
					minn = g[1][j];
					id = j;
				}
			}
		}
		sum += minn;
		v[{1, id}] = 1;
		v[{id, 1}] = 1;
	}
	int t = cntb;
	while (t < s) 
	{
		s--;
		for (rint i = 0; i <= 25; i++) f[i] = {0, 0, -1};
		dfs(1, 0);
		int maxx = 0, id = 0;
		for (rint j = 2; j <= tot; j++) 
		{
			if (g[1][j] && maxx < f[j].dist - g[1][j]) 
			{
				maxx = f[j].dist - g[1][j];
				id = j;
			}
		}
		if (!maxx) break;
		v[{f[id].a, f[id].b}] = v[{f[id].b, f[id].a}] = 0;
		v[{1, id}] = v[{id, 1}] = 1;
		sum -= maxx;
	}
	cout << "Total miles driven: " << sum << endl;
	return 0;
}

AcWing348. 沙漠之王

簡要題意

給這一張 \(N\) 個點 \(M\) 條邊的無向圖,圖中每條邊 \(e\) 都有一個收益 \(C_e\) 和一個成本 \(R_e\),求該圖的一顆生成樹 \(T\), 使樹中各邊的收益之和除以成本之和,即 \(∑_{e∈T}C_e/∑_{e∈T}R_e\) 最大。\((1<N,M<10000)\)

題目分析

\(x=w/l\)\(w-l*x =0,f(x) = w-l*x\);將邊權更改為 \(w-l*x\) 來求生成樹

因為 \(f(x)\) 是個單調遞減函式,隨著 \(x\) 的增大而減少,對於任意一個生成樹如果 \(f(x)>0\),則 \(l\) 需要增大 \(f(x)<0\) 否則 \(l\) 需要減小 若要滿足 \(f(x)==0\) 恆成立

1.若要 \(x\) 取最大值,則不能存在任意一個生成樹 \(f(x)>0\), 否則 \(x\) 還能繼續增大,即任意生成樹 \(f(x)<=0\) 若存在一個生成樹 \(f(x)>0\),則那個生成樹的比率一定大於當前 \(x\), \(w/l > x\)\(w-l*x > 0\)

2.若要 \(x\) 取最小值,則不能存在任意一個生成樹 \(f(x)<0\),否則 \(x\) 還能繼續減小,即任意生成樹 \(f(x)>=0\) 若存在一個生成樹 \(f(x)<0\),則那個生成樹的比率一定小於當前 \(x\), \(w/l < x\)\(w-l*x < 0\)

若要滿足 \(f(x)>0\) 恆成立,則最小生成樹 \(>0\)

若要滿足 \(f(x)<0\) 恆成立,則最大生成樹 \(<0\)

此題目求解最小的 \(x\) 值,也就是檢查是否所有的生成樹 \(f(x)>=0\),即最小生成樹 \(>=0\)

如果最小生成樹大於 \(0\),所有的生成樹都滿足 \(f(x)>0\), 嘗試增加 \(x\) 得到 \(f(x)=0\)

否則,有生成樹不滿足這個條件,那麼 \(x\) 一定要減少來使所有 \(f(x)>=0\)

double calc(int a, int b) 
{
	return sqrt((x[a] - x[b]) * (x[a] - x[b]) + (y[a] - y[b]) * (y[a] - y[b]));
}

bool check(double mid) 
{
    fill(dist, dist + n + 1, dinf);
    fill(v, v + n + 1, 0);
	dist[1] = 0;
	double ans = 0;
	for (rint i = 1; i <= n; i++) 
	{
		int x = 0;
		for (rint j = 1; j <= n; j++)
		{
			if (!v[j] && (x == 0 || dist[j] < dist[x])) x = j;		
		}
		v[x] = 1;
		ans += dist[x];
		for (rint y = 1; y <= n; y++) 
		{
			if (!v[y]) dist[y] = min(dist[y], fabs(w[x] - w[y]) - mid * calc(x, y));
		}
	}
	return ans >= 0;
}

signed main() 
{
	while (cin >> n && n) 
	{
		for (rint i = 1; i <= n; i++)
		{
			cin >> x[i] >> y[i] >> w[i];
		}
		double l = 0, r = 10000000;
		double ans;
		while ((r - l) > eps) 
		{
			double mid = (l + r) / 2;
			if (check(mid)) ans = mid, l = mid;
			else r = mid;
		}
		cout << fixed << setprecision(3) << ans << endl;
	}
	return 0;
}

AcWing.349. 黑暗城堡

題目大意

問你有多少棵最短路徑樹

題目分析

這裡用的鄰接矩陣 Dijkstra

對於已經是最短路的情況,對於任意兩個點 \(x,y\)\(dist[y] <= dist[x] + z\)

現在考慮 \(dist[y] <= dist[x] + z\),那麼在最短路徑生成樹中一定不能有這一條邊。如果有這一條邊,那麼 \(y\) 的路徑就不是最小的。(因為是樹,所以只能是這一個點來對 \(y\) 進行更新)

那麼當 \(dist[y]==dist[x]+z\),最短路徑生成樹裡面可以包含這一條邊。

1.對於每一個點,都有到達 \(1\) 號點的距離。現在按照距離從小到大對點進行考慮。

2.對於考慮到的 \(i\) 個點,查詢已經遍歷過的集合,看有多少 \(x\) 滿足 \(dist[i]==dist[x]+z\)。這是方案數。使用乘法原理。

int n, m;
int a[N][N];
int dist[N];
int ans = 1;
bool v[N];
pair<int, int> f[N];

signed main()
{   
    cin >> n >> m;
	memset(a, 0x3f, sizeof a);
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    
	for (rint i = 1; i <= n; i++) a[i][i] = 0;
    for (rint i = 1; i <= m; i++)
    {
        int x, y, z;
        cin >> x >> y >> z;
        a[x][y] = a[y][x] = min(a[x][y], z);
    }
    
    for (rint i = 1; i <= n; i++)
    {
        int x = 0;
        for (rint j = 1; j <= n; j++)
            if (!v[j] && (x == 0 || dist[x] > dist[j])) x = j;
        v[x] = 1;
        for (rint y = 1; y <= n; y++)
        {
            if (!v[y]) dist[y] = min(dist[y], dist[x] + a[x][y]);   
        }
    }
    
    for (rint i = 1; i <= n; i++) f[i] = {dist[i], i};
    sort(f + 1, f + n + 1);
    
    memset(v, 0, sizeof v);
    v[1] = 1;
    for (rint i = 2; i <= n; i++)
    {
        int y = f[i].second;
        int cnt = 0;
        for (rint x = 1; x <= n; x++) 
        {
            if (v[x] && dist[x] + a[x][y] == dist[y]) cnt++;
        }
        ans = ans * cnt % mod;
        v[y] = 1;
    }
    cout << ans << endl;
    return 0;
}

AcWing.388. 四葉草魔杖

題目大意

給定一張無向圖,結點和邊均有權值。所有結點權值之和為 \(0\),點權可以沿邊傳遞,傳遞點權的代價為邊的權值。求讓所有結點權值化為 \(0\) 的最小代價。

題目分析

容易想到本題與最小生成樹有關。一種不難想出的思路是求出原圖的最小生成樹,將最小生成樹上所有邊的權值之和作為答案。

但經過思考,可以發現這樣得到的不一定是最優解。首先,原圖可能並不聯通;其次,可以將原圖劃分為若干個點權之和均為 \(0\) 的子圖,在這些子圖中分別轉移點權,最後將答案合併。這樣得到的方案或許會更優。

此時我們發現劃分方案不止一種,如何確定最終的方案成了需要解決的最大問題。

注意到本題中 \(N\) 範圍較小,允許我們把所有點權和為 \(0\) 的子圖(以下簡稱“合法子圖”)的最小生成樹全部求出。因此可以先列舉原圖點集的所有子集,對於每個點權和為 \(0\) 的點集,用這些點和連線它們的邊構造一張合法子圖。我們能夠輕易求出這些合法子圖的最小生成樹。但有些合法子圖或許並不聯通,為避免對之後的求解造成影響,需要把這些子圖的最小生成樹邊權和設為 \(\infty\)

接下來需要把這些子圖中的若干個合併起來,得到全域性最優解。與劃分的情形相同,合併這些子圖的方案也有多種。可以使用 \(DP\) 得到最優解。

具體地,考慮進行類似揹包的 \(DP\),將每個合法子圖視作可以放入揹包的一個物品。設 \(A\)\(B\) 為兩個不同合法子圖的點集,合法子圖的最小生成樹邊權和為 \(S\),可以寫出如下狀態轉移方程:

$f_{A \cup B}=min {f_{A\cup B}, f_{A}+S_{B} },A\cap B=\oslash $

最終 \(f_{2^n-1}\) 即為所求的答案。

int n, m;
int a[N], fa[N];
int s[M], f[M], p[M];

struct rec 
{
	int x, y, z;
	friend bool operator < (rec a, rec b)
	{
		return a.z < b.z;
	}
} edge[M];

int find(int x) 
{
	return fa[x] == x ? x : fa[x] = find(fa[x]);
}

int kruskal(int s) 
{
	int ans = 0;
	for (rint i = 0; i < n; i++) if (s & (1 << i)) fa[i] = i;
	for (rint i = 1; i <= m; i++) 
	{
		if (!(s & (1 << (edge[i].x))) || !(s & (1 << (edge[i].y)))) continue;
		int x = find(edge[i].x);
		int y = find(edge[i].y);
		if (x == y) continue; 
		fa[x] = y;
		ans += edge[i].z;
	}
	int father = -1;
	for (rint i = 0; i < n; i++)
	{
		if (s & (1 << i))
		{
			if (father == -1) father = find(i);
			else if (find(i) != father) return inf; 			
		}
	}
	return ans;
}

signed main() 
{
	cin >> n >> m;
	
	for (rint i = 1; i <= n; i++) cin >> a[i];
	for (rint i = 1; i <= m; i++) cin >> edge[i].x >> edge[i].y >> edge[i].z; 
		
	for (rint i = 1; i < (1 << n); i++)
		for (rint j = 0; j < n; j++)
			if (i & (1 << j))
			    s[i] += a[j + 1]; 	

	sort(edge + 1, edge + m + 1);
	for (rint i = 1; i < (1 << n); i++) 
	{
		if (!s[i]) p[i] = kruskal(i); 
		f[i] = inf;
	}
	f[0] = 0;
	for (rint i = 1; i < (1 << n); i++) 
	{ 
		if (s[i]) continue;
		for (rint j = 0; j < (1 << n); j++)
		{
			if (!(i & j)) f[i | j] = min(f[i | j], f[j] + p[i]);			
		}
	}
	if (f[(1 << n) - 1] >= inf) puts("Impossible");
	else cout << f[(1 << n) - 1] << endl;
	
	return 0;
}

相關文章