1. 程式人生 > 其它 >【貪心】AcWing 115. 給樹染色

【貪心】AcWing 115. 給樹染色

傳送門:https://www.acwing.com/solution/content/82014/

分析

假如沒有染色順序的約束,那麼最佳決策當然是先染權值大的點(本質上就是排序不等式)。

然而現在它有約束,但我們可以保證的一點是:當樹上最大的點 \(u\)父節點 \(p\) 被染色的時候,立刻將 \(u\) 進行染色是最優的:這意味著,我們可以將其合併成一個新的點,等價於染色步驟中連續對這兩個點進行染色。

那麼這個新點的權值應該是多少呢?考慮現在 \(X,Y\) 點組成一個合併的點(對應上述的 \(p, u\)\(X\) 可以被染色,然後 \(Z\) 點也是當前可以被染色的點。(記它們點權為自己的小寫字母)

假設現在已經染了 \(k\) 個點

  • 如果先染 \((X, Y)\),那麼貢獻是 \((k+1)x + (k+2)y + (k+3)z\)
  • 如果先染 \(Z\),貢獻為 \((k+1)z + (k+2)x + (k+3)u\)

也就是合併的 \((X, Y)\)\(Z\) 染色的優先順序取決於兩個貢獻的大小。

\((k+1)x + (k+2)y + (k+3)z < (k+1)z + (k+2)x + (k+3)y\) 等價於 \((x + y)/2 > z\),即 \((x + y)/2 > z\) 時選擇先染 \(Z\) 否則先染 \((X,Y)\)

據此,將一個新點

權值設定為兩點平均值是不改變決策的優先順序的,推而廣之,如果一個新點是由兩個新點合併而來,那麼權值就是兩點的權值和的平均值(例如一個新點 \(A\)\(a\)起初樹上的點,\(B\)\(b\)起初樹上的點,那麼權值就是 \((sum_A + sum_B) / (a + b)\)

實現

y總 的做法是利用偏移量進行統計,感覺不太好想 qwq,所以我採取的是將合併的過程進行模擬,並記錄相關資訊,最後對剩下那個點的資訊進行展開得到決策序列(有點像解壓縮)。

寫這題的時候心很慌,感覺自己的實現挺亂的(感覺就我是這樣亂搞的),沒想到能 1A。如果是因為資料水了可以來試試 hack qwq。

看程式碼:

// Problem: 給樹染色
// Contest: AcWing
// URL: https://www.acwing.com/problem/content/117/
// Memory Limit: 64 MB
// Time Limit: 1000 ms
// 
// Powered by CP Editor (https://cpeditor.org)

#include<bits/stdc++.h>
using namespace std;

#define debug(x) cerr << #x << ": " << (x) << endl
#define rep(i,a,b) for(int i=(a);i<=(b);i++)
#define dwn(i,a,b) for(int i=(a);i>=(b);i--)
#define pb push_back
#define all(x) (x).begin(), (x).end()

using pii = pair<int, int>;
using ll = long long;

inline void read(int &x){
    int s=0; x=1;
    char ch=getchar();
    while(ch<'0' || ch>'9') {if(ch=='-')x=-1;ch=getchar();}
    while(ch>='0' && ch<='9') s=(s<<3)+(s<<1)+ch-'0',ch=getchar();
    x*=s;
}

const int N=2022;

int n, rt;
int w[N];

struct Node{
	int fa, sz, sum;
	double avg;
}e[N];

int tot;
int h[N], ne[N]; // 記 cur=(p, u), 那麼 h[cur]=p, ne[cur]=u
bool vis[N], inr[N]; // 分別代表:是否已經被併入新點,是否在根節點所在的新點中

int find(){
	int u; double val=0;
	rep(i,1,tot) if(!vis[i] && !inr[i] && e[i].avg>val) val=e[i].avg, u=i;
	return u;
}

vector<int> get(int cur){ // 解壓縮
	int p=h[cur], u=ne[cur];
	vector<int> v1, v2;
	if(p>n) v1=get(p);
	else v1={p};
	
	if(u>n) v2=get(u);
	else v2={u};
	
	v1.insert(end(v1), all(v2));
	return v1;
}

vector<int> g[N];

void getFa(int u, int fa){
	e[u].fa=fa;
	for(auto go: g[u]) if(go!=fa) getFa(go, u);
}

int main(){
	cin>>n>>rt;
	rep(i,1,n) read(w[i]), e[i].sum=e[i].avg=w[i], e[i].sz=1;
	
	inr[rt]=1;
	rep(i,1,n-1){
		int u, v; read(u), read(v);
		g[u].pb(v), g[v].pb(u);
	}
	
	getFa(rt, 0); // 將每個點的父親處理出來
	
	tot=n;
	rep(_,1,n-1){
		int u=find(), p=e[u].fa; // 找到 u 以及 p
		vis[u]=vis[p]=1; // 標記為已合併

		tot++;
		e[tot]={e[p].fa, e[u].sz+e[p].sz, e[u].sum+e[p].sum, (double)(e[u].sum+e[p].sum)/(e[u].sz+e[p].sz)};
		h[tot]=p, ne[tot]=u;
		if(inr[p]) inr[u]=inr[tot]=1;
		rep(i,1,tot-1) if(e[i].fa==u || e[i].fa==p) e[i].fa=tot; // 維護髮生修改的每個點父節點資訊
	}

	auto vec=get(tot); // 從合併得到的最後的新點(此時樹上只剩一個點)開始解壓縮得到合併的操作序列。
	
	int res=0;
	rep(i,1,n) res+=i*w[vec[i-1]];
	cout<<res<<endl;
	
	return 0;
}