1. 程式人生 > 實用技巧 >很可能是假做法的:口胡【WC2012】最小生成樹(Kruskal+Tarjan)

很可能是假做法的:口胡【WC2012】最小生成樹(Kruskal+Tarjan)

Link:
Luogu https://www.luogu.com.cn/problem/P4120


Description


以一定代價修改帶權無向圖邊權使得最小生成樹唯一,要求最小化代價。
其中,邊權減一代價 \(a\),加一代價 \(b\)

弱化版:CF1108F MST Unification

我會直接地把我的思路全部寫在這裡。
但是我很菜,所以全程都是非常混沌的思考,假得很厲害。
建議跳到 Step 9 開始閱讀。
或者直接跳到 Step 11 觀看假做法(

僅提供一點可能的思路,已經想了好久了
做點別的,不想繼續做這題了。。

不要越級打歌


Solution


Step 1

一些大家都知道的基本事實

致死量廢話

不唯一代表一定能找到一條邊
注意到資料範圍
這個資料範圍只允許常數次Kruskal或者分塊Prim

(接下來的割邊不是傳統的那個意思,我只是沒詞了)
(本文暫時稱“將全體點的集合分為兩個點集”這樣的分割為一個“割”,然後原本連線這兩個點集的邊稱為“割邊”)
一條邊明顯可以作為多個割的割邊。
思考:是否只要某個割有多條最短割邊就代表有多個 mst?
顯然是的。
思考:如果把這些最短割邊隨機去了,每個割只留一條,就會得到唯一的 mst?

這樣會不會不小心誤傷,然後得到一個不連通的圖呢?
不會

可惜的是,不能真的去掉。
然而可以發現一個事實:完全可以把其他割邊集體+1或者單獨給要留下的割邊-1
但是呢!但是呢!這樣的話!可能會誤傷到之前操作過的割邊!

啊這

好吧,總之我們先走一個Kruskal

給阿姨倒杯卡布奇諾


Step 2

關於加只會加一和大量廢話

Kruskal 證明正確性的時候,就經常遇到一個環的場景
假如我們現在差點要被環了咋辦呢?
因為在跑 Kruskal,所以貌似直接進行一個一的加就可以了
反正之後再碰到這個加進來就會成環的邊也可以再進行一個一的加
有可能再碰到嗎?
不行吧?一個是,還會再碰到不如直接放進來
另外一個是,再碰到就意味著你這個 mst 連出了環
我直接o(*≧▽≦)ツ

乾脆不要連成這樣可能可以避過這種可能成環的情況?
另外的連法,一種是把環的一部分仍然放在環裡做一波輪換,挺沒意義的還得多費工夫
另一種是直接把環斷了然後經過外面的點再連通
因為在做 Kruskal 所以……只會更麻煩
所以改變連法只會使得代價更高。
直接把那個補上就成環的邊加一即可,反正也只會有那一條。

哈哈
但是呢,現在可以減
也就是說,把原來的(成環預備軍裡的)最長邊都減一也是可以的,傻了吧
而且這可能還可以對解決其他地方可能發生的成環情況起到幫助……?


Step 3

一些廢話

好複雜啊,要不我們直接隨機化吧
現在可以確定的是,要加只會有上面那一種情況。而且只會加一。

問題就集中到了減身上了。

另外呢。
我最開始有一種思路,就是直接跑 Kruskal,欽定一個最小生成樹,
並且把所有可能替換 被欽定的mst中的邊 的那些邊都鯊了

如果是加一,那其實很好做,反正加了之後這些邊就真的被鯊了
但是減一的話可能會干擾之前的結果(不確定?)

總之。
問題就集中到了減身上了。


Step 4

關於減只會減一

來思考一下幾個常見的結論
圖中任意一個點連出去的所有邊,邊權最小的一條至少存在於一棵最小生成樹上。
任意一個無向圖的最小生成樹中每種權值的邊的數量是一定的。

再思考一下
減真的會造成干擾嗎

兄啊,不是啊,這東西咋干擾啊,又不能把之前的那個選了的邊替掉,又不能讓哪條沒選的復活


Step 5

關於有重邊的那些事

寫吧寫吧
放棄幻想,準備鬥爭

經典先wa再改
好,只過了兩個點,我回來了
可以確定的是資料有重邊;邊權一開始均為正。

按理來說我是覺得沒啥問題了
或許是可以把某條邊權值大力下降來強制 mst 過它?
我思考了一下,反正找到這條邊的時候仍然是那個經典場景,不如直接給它加一去了
面向資料地,\(b\) 遠大於 \(a\) 的資料點我的答案相差比較多
……好像也不是。

我仔細想了一下,發現我重邊沒去到位,我紫菜了
去完了,還是過不了


Step 6

關於割邊不用管的那些事

然後我對著資料7看了一下
好傢伙
所以,就算我們現在先考慮不帶權 mst 也
並不是只有把整個 mst 減一或者不是 mst 的全部加一這兩個選項。
(沒錯,我前面提到的做法當不帶權時就會退化成這個樣子)

當然這很明顯比如說有一個割(最開頭那個定義)只有一條割邊,那肯定直接選上,根本不用加加減減
但是呢!
這樣代表,是割邊的全部可以不用管

好活,然後我的答案疑似比正解小了
也可能太大了,大到多了幾個位

假做法的最高境界
“此處顯然” → 爆炸

然後我突然發現,我前面常常誤解了,成環的時候到底會多出多少個 mst。


Step 7

這一步就是一個假做法,程式碼也是假的

銜接上一步,現在在去掉重邊以及去掉割邊之後再繼續考慮:

我突然發現一個令人震驚的事實
假如把最小生成樹除了割邊(傳統意義)全部減一遍,那麼絕壁唯一
可以不用減這麼多邊嗎?當然可以啦,比如說你一開始給出的圖就是樹。。。

然後呢,我們在樹的基礎上新增邊形成環。跑一下克魯斯卡爾。
那麼,比如說假如吧這環裡兩條邊權值相等
要不把裡面一條減一,要不把另一條加一,然後這兩條再也不用管了

回到剛才再來一次,現在比如說假如吧這環裡三條邊權值相等
要不把裡面一條加一,要不把另外兩條都減一。

好吧?
懂了嗎?
懂了嗎??
這操作要在並查集上面進行啊

求割邊其實完全沒有意義(
因為割邊根本就不可能在環裡邊

掛一個錯誤程式碼。

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<cstdlib>
#include<queue>
#include<vector>
using namespace std;
#define rep(ii,jj,kk) for(int ii=jj;ii<=kk;++ii)
const int MAXN = 5e5 + 10;
const int MAXM = 1e6 + 10;
int n, m, tot = 0;
int fa[MAXN], siz[MAXN], mxx[MAXN], mxc[MAXN];
int Qst = 1, Qed = 0;
long long Ans = 0;
long long tmp1, tmp2;
//const int XDM = 2e6 + 10;
//int head[MAXN], nxt[XDM], to[XDM], ixd[XDM], val[XDM];
//#define add_edge(u,v,w,idd) nxt[++tot]=head[u],head[u]=tot,to[tot]=v,val[tot]=w,ixd[tot]=idd
int findfa(const int& x)
{
    return fa[x] == x ? x : (fa[x] = findfa(fa[x]));
}
int fx, fy;
//void merge(const int& x, const int& y)
//{
//	fx = findfa(x), fy = findfa(y);
//	if(fx==fy)return;
//	if(siz[fx]<siz[fy])swap(fx,fy);
//	fa[fy]=fx,siz[fx]+=siz[fy];
//}
struct Edge
{
	int ptf/*from*/, ptt/*to*/, ptv/*value*/;
	Edge(int aug1 = 0, int aug2 = 0, int aug3 = 0)
	{
		ptf = aug1;
		ptt = aug2;
		ptv = aug3;
	}
}tQ[MAXM], Q[MAXN];
//bool operator > (const Edge &a, const Edge &b)
//{
//	return a.ptv > b.ptv;
//}
bool operator == (const Edge&a, const Edge&b)
{
	return ((a.ptf==b.ptf)&&(a.ptt==b.ptt));
}
//bool cmpEdg(const int&a, const int&b)
//{
//	return tQ[a].ptv<tQ[b].ptv;
//}
//priority_queue<int, vector<int>, cmpEdg> P;
bool cmpvt(const Edge&a, const Edge&b)
{
	if(a.ptf==b.ptf)
	{
		if(a.ptt==b.ptt)
		{
			return a.ptv<b.ptv;
		}
		return a.ptt<b.ptt;
	}
	return a.ptf<b.ptf;
}
bool cmp(const Edge&a, const Edge&b)
{
	return a.ptv < b.ptv;
}
int a, b, inin = 0, excv = 0, sub = 0, lnk = 0;
int sma = 0;
//int dfn[MAXN], low[MAXN], mxx[MAXN], mxc[MAXN];
//int ttot = 0;
//void tarjan(int x, int fa)
//{
//	dfn[x] = low[x] = ++ttot;
//	for(int i=head[x];i;i=nxt[i])
//	{
//		if(to[i]==fa)continue;
//		if(!dfn[to[i]])
//		{
//			P.push(idx[i]);
//			tarjan(to[i],x);
////			if (low[to[i]] > dfn[i]);
//		}
//		else
//		{
//			low[x]=min(low[x],dfn[to[i]]);
//			fx=findfa(x);
//			tmp1=b;
//			tmp2=1ll*a*mxc[fx];
//		}
//	}
//}
int main()
{
	string str;
	int id;
	cin >> str >> id;
	scanf("%d%d%d%d", &n, &m, &a, &b);
	rep(i, 1, n) siz[i] = 1, fa[i] = i;
 	for(int u, v, w, i = 1; i <= m; ++i)
	{
		scanf("%d%d%d", &u, &v, &w);
		if (u == v) continue;
		if (u > v) swap(u, v);
		Q[++sub] = Edge(u, v, w);
	}
	m = sub;
	sort(Q+1, Q+1+m, cmpvt);
	sub = 0;
	for (int i = 1; i <= m; ++i)
	{
		if (i > 1) 
		{
			if (Q[i] == Q[i-1])
			{
				if (!sma) sma = Q[i-1].ptv;
				if (Q[i].ptv == sma) ++lnk;
				continue;
			}
			else if (lnk)
			{
				tmp1 = a;
				tmp2 = 1ll * b * lnk;
				Ans += (tmp1 < tmp2) ? tmp1 : tmp2;
				sma = 0;
				lnk = 0;
			}
		}
		tQ[++sub] = Q[i];
	}
	if (lnk)
	{
		tmp1 = a;
		tmp2 = 1ll * b * lnk;
		Ans += (tmp1 < tmp2) ? tmp1 : tmp2;
	}
	m = sub;
	sort(tQ+1, tQ+1+m, cmp);
//	rep(i,1,m)add_edge(tQ[i].ptf,tQ[i].ptt,tQ[i].ptv,i),add_edge(tQ[i].ptt,tQ[i].ptf,tQ[i].ptv,i);
//	rep(i,1,n)if(!dfn[i])tarjan(i);
	for (int i = 1; i <= m; ++i)
	{
    	fx = findfa(tQ[i].ptf);
    	fy = findfa(tQ[i].ptt);
    	if (fx == fy)
    	{
    		if (Q[i].ptv != mxx[fx]) continue;
    		tmp2 = 1ll * a * mxc[fx];
    		if (b < tmp2) Ans += b;
    		else Ans += tmp2, --mxx[fx];
    	}
    	else
    	{
    		if(siz[fx] < siz[fy]) swap(fx, fy);
    		fa[fy] = fx;
    		siz[fx] += siz[fy];
    		if (mxx[fx] == tQ[i].ptv)
    		{
    			if (mxx[fx] == mxx[fy]) mxc[fx] += mxc[fy];
    			else if (mxx[fx] < mxx[fy]) mxx[fx] = mxx[fy], mxc[fx] = mxc[fy];
    			++mxc[fx];
    		}
    		else mxx[fx] = tQ[i].ptv, mxc[fx] = 1;
    	}
	}
	printf("%lld", Ans);
	return 0;
}

我寫了個什麼東西
順帶一提我發現我之前並查集按秩合併還寫錯了
回到上面那一段,我剛剛說那兩條再也不用管了
為啥,因為再碰上這種事情就是再成了環……

……但是可以成環啊。
的確可以啊,再次成環的時候要再整掉。
之前被加一的那條會產生影響嗎?
如果之前減一了會產生影響嗎?


Step 8

這一步第一段提出了一個假做法

不如在 Kruskal 的時候,假如生成樹可能不唯一了也給它保留下來
然後:找邊雙?
仔細想了想還是沒能跳出那個圈裡
那行吧。
我來對拍了?稍微把權值範圍限制小一些,然後整一波暴力。

等一等啊,我把資料⑩改了一下又發現了一個問題
我直接用並查集維護“環裡的”當前最大邊權以及這樣的最長邊數量
然後就把環外面的也給拉下場了

那麼現在剩下的唯一的問題應該就是,到底怎麼正確地維護這個東西。
這個東西很明顯地和 Kruskal 衝突,所以不能同時做。


Step 9 基本原理

明確目標和一些失敗的嘗試

現在暫時先不懷疑基本演算法的正確性了。
實際上我懷疑了好多次到最後只不過是我自己太菜實現錯了而已。
當然時間複雜度可能有需要懷疑一下(

重新複述一遍,就是一邊 Kruskal,在每次差點要成環的時候,
記那個Kruskal正在處理的邊為 v,然後
看一看這個即將成環的東西(就是環去掉v)裡面有幾個跟v權值相同
然後考慮是把這幾個權值-1還是給v權值+1。

我暫且矇在鼓裡。
不過貌似吧,貌似啊,
這個環
實際上在現在的mst裡邊
就是 葉子1 ↔ lca ↔ 葉子2 ↔ v ↔ 葉子1

好傢伙,1e6可以倍增嗎?(雖然排序倒是可以)
而且還得維護啥啊……維護max??加上是max的cnt
不現實吧。

也許也可能跟擬陣有關?

有一個想法是去掉割邊然後按DFS序,但是Kruskal會打亂
Borůvka能不能做?
每次連線兩個連通塊如果有多個選擇就考慮一下減或者加。好像很可以。
實際上完全不行,仍然不能處理環。


Step 10 嘗試找環

這一步提出了一個假做法

……
考慮一個純粹的環。
完全可以直接決定它的生成樹

但是呢,環這種東西啊
比如說“一個大環裡面有一個小環”,實際上是假的,小環也可以掰出去

但是呢但是呢
兩個環套一起就是要去兩次邊。

但是呢但是呢但是呢
解決了一個環,新環可能會帶來更小的邊
然後問題就大了,可能不得不開桶。
也可以嘗試把環按照權值最大邊排序??這。。
總之一邊Tarjan求割邊,一邊做Kruskal的想法破產了


Step 11 實現原理

這一步或許提出了一個假做法

Kruskal必須套在最外層。
這樣吧,不妨按照權值從小到大。弄分層圖?
每次把這個權值所有邊建圖。看看有沒有形成環。
沒有的話,縮點(強連通分量而非邊雙)。然後進行下一輪。
有的話,老套路做一波加或者減更新答案。然後縮點進入下一輪。
環的事情用Tarjan就可以處理。(經典Tarjan找環)
因為這裡限定了每次只對一種權值的邊進行處理,所以不必單獨維護mxx和mxc(就是環裡面最大權邊的權值和數量)
(在Tarjan找環的時候就可以順便弄到這兩個東西)

思路來自於 牛客練習賽32-D

我就鴿了。
順帶一提,本題邊權貌似最大可以達到一千,所以開桶是不行的。
總覺得沒有什麼新科技或者高階資料結構怪怪的

假做法寫部落格是不是不太好啊
我什麼時候補上吧,再假了再說。。再說。。