[NOIP2017 提升組] 寶藏

[NOIP2017 提升組] 寶藏


狀態壓縮好題。下面咱們來一一分析一下這道題的整體想法。ios


容易發現,\(n\leq12\)暗示這道題時間複雜度爲指數級別的。c++

因爲題目代表測試

兩個已經被挖掘過的寶藏屋之間的道路無需再開發。優化

因此,咱們開發的路徑構成的只能是一棵樹,不能是有環的圖。spa

所以,咱們能夠將題目抽象成:給定一張有\(n\)個點的圖,求出一棵生成樹(根節點任意)使 \(\sum_{u\in G,fa_u\in G} dep_u\times w(u,fa_u)\)最小。code

因爲\(n\)很小,不難想到可使用狀態壓縮。ip

我最開始的想法是:枚舉根節點\(root\),並定義以當前的根爲根節點的狀態:\(d(i,j,S)\)表明當前葉子結點\(j\)\(root\)距離爲\(i\)時,正處理的點集\(S\)的最小值。開發

那麼\(i\geq2\)時有以下轉移:\(dp(i,j,S)=min(dp(i-1,k,S\setminus\{i\})+i\times d(i,k))\)get

\(j=1\)有:\(dp(i,j,S)=min(dp(x,k,S\setminus\{i\})+d(j,root))\)string

初始化:讓\(dp(0,root,0)=0\),其他\(\infty\)

這樣的作法效率大約是:\(O(n^42^{n})\),實際測試中全部的狀態是跑不滿的,故這種作法是能夠經過本題目的時間限制的。


咱們注意到剛剛那個轉移是至關混亂的,而且它會重複計算狀態值。

咱們上述作法本質上是利用葉子結點向上逆推推的過程。那麼,咱們是否能夠反過來想——從一個結點向它的子結點擴展,考慮剩下的子結點集合的代價。

根據此,咱們來從新定義以前的狀態表示:

\(dp(i,j,S)\)表明距離根節點距離\(i\)考慮到的當前結點爲\(j\),從\(j\)開始「挖」的點集爲\(S\)的最小代價和。換句話來說,咱們只考慮\(j\)爲根的子樹的代價。

那麼有:\(dp(i,j,S)=min(dp(i+1,k,S'\setminus\{k\})+dp(i,j,S\setminus{S'})+d(j,k))\),這裏面的\(S'\)表明的是\(S\)中的子集,而k是S‘中的元素。

不難觀察到,方程相似於樹形DP中子樹合併這一重要思想。事實上,咱們利用它解決過樹形依賴關係的一類問題(揹包)。

初始化,咱們讓\(dp(i,j,0)=0\),其他正無窮,最終答案爲\(min(dp(0,i,U\setminus{i}))\)。時間複雜度爲\(O(n^32^n)\),具體計算過於複雜,在此不展開。

在實現的過程當中,爲優化常數,咱們能夠預處理出全部狀態下的元素個數、最小元素下標,以及將全部的點的下標平移。

另外,要注意邊界條件。

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#define RE register
#define CLR(x, y) memset(x,y,sizeof x)
#define FOR(i, x, y) for(RE int i=x;i<=y;++i)
#define ROF(i, x, y) for(RE int i=x;i>=y;--i)
using namespace std;
const int N = 13, S = 1 << N, INF = 1e9 + 5;
typedef long long LL;
template <class T> 
void read(T &x)
{
	bool mark = false;
	char ch = getchar();
	for(; ch < '0' || ch > '9'; ch = getchar()) if(ch == '-') mark = true;
	for(x = 0; ch >= '0' && ch <= '9'; ch = getchar()) x = (x << 3) + (x << 1) + ch - '0';
	if(mark) x = -x;
	return;
}
int n, m, t, siz[S] = {}, tt[S] = {};
LL ans = INF, d[N][N], dp[N][N][S];
int main()
{
	read(n), read(m);
	t = 1 << n, -- t;
	FOR(i, 0, n) 
	{
		d[i][i] = 0;
		FOR(j, i + 1, n) d[i][j] = d[j][i] = INF;
	}
	LL u, v, w;
	FOR(i, 1, m)
	{
		read(u), read(v), read(w);
		-- u,  -- v;
		d[u][v] = d[v][u] = min(d[u][v], w);
	}	
	FOR(i, 1, t) siz[i] = siz[i & (i - 1)] + 1;
	for(int i = 0; i < n; ++ i) tt[1 << i] = i;
	FOR(i, 1, t) tt[i] = tt[i & (-i)];
	CLR(dp, 0x3f);
	for(int i = 0; i < n; ++ i)
		for(int j = 0; j < n; ++ j)
			dp[i][j][0] = 0;
	for(int i = n - 2; i >= 0; -- i)
	{
		for(int j = 0; j < n; ++ j)
		{
			FOR(s, 1, t)
			{
				if((s & (1 << j)) || siz[t ^ s] <= i) continue;
				for(int S1 = s; S1; S1 = (S1 - 1) & s)
				{
					for(int tmp = S1, k = tt[S1]; tmp; k = tt[tmp ^= tmp & (-tmp)])
						if(d[j][k] < INF) dp[i][j][s] = min(dp[i][j][s], dp[i + 1][k][S1 ^ (1 << k)] + dp[i][j][s ^ S1] + (i + 1) * d[j][k]);
				}
			}
		}
	}
	for(int i = 0; i < n; ++ i) ans = min(ans, dp[0][i][t ^ (1 << i)]);
	printf("%lld\n", ans);
	return 0;  
}

總結:

  1. 當數據規模很小的時候,能夠考慮狀態壓縮
  2. 在考慮樹上問題時每每從根節點開始考慮(像倍增一類的題目是從本身點開始考慮);
相關文章
相關標籤/搜索