「NOIP2017」寶藏


「NOIP2017」寶藏 題解

博客閱讀效果更佳


又到了一年一度NOIPCSP-S 賽前復習做真題的時間

於是就遇上了這道題


首先觀察數據范圍 \(1 \le n \le 12\) ,那么極大可能性是狀壓 \(\texttt{DP}\) 或者 \(\texttt{DFS}\) 爆搜

但由於這題放在了 \(\texttt{DP}\) 列表里面,於是優先考慮狀壓

簡化題意:

從給定的 \(n\) 個點,\(m\) 條邊的有重邊的無向聯通圖中,找出一棵生成樹,使得題目所求價值最小

從題目給出的建邊價值來看,我們發現一條邊的價值跟以下幾點有關:

  • 根的位置
  • 當前狀態下的樹的高度
  • 該邊的長度

邊的長度不能改變,根的位置並不能很好的作為 \(\texttt{DP}\) 時候的階段,所以我們考慮以樹的高度作為DP的階段

設根的深度為1

\(\texttt{f[i][j]}\) 表示 當前樹的高度為 \(i\) ,已經選了的點集的集合為 \(j\),那么狀態轉移方程即為

\[f[i][j]=\min_{k\ \in \ j}(f[i-1][j \ \mathrm{xor} \ k]+dis[j \ \mathrm{xor} \ k][k]\cdot(i-1)) \]

其中異或操作在這里是取補集的意思,\(\texttt{dis[i][j]}\) 表示從 \(i\) 這個已選點集加上下一層將要選的 \(j\) 這個點集所需要的最小花費

那我們應該如何完善 \(\texttt{dis}\) 數組呢

先給出遞推式

\[dis[i][j]=dis[i][j \ \mathrm{xor} \ \mathrm{lowbit}(j)]+\min_{k=1}^{n\ \&\& \ (1<<k)\&i}(d[\log_2\mathrm{lowbit}(j)+1][k]) \]

其中 \(d\) 數組表示第 \(i\) 個點到 第 \(j\) 個點的道路長度(沒有則為 \(\infty\) ),\(j\)\(i\) 的補集的任一子集

然后從小到大枚舉 \(j\) ,就能夠保證順序正確(因為 \(j \ \mathrm{xor} \ \mathrm{lowbit}(j)\) 一定比 \(j\) 要小)

因為每一次更新只涉及到一個點的更改,所以不難得出這樣預處理 \(dis\) 數組的正確性

然后,這題就完了

另外還有幾點需要注意的

  • 邊最好使用鄰接矩陣儲存,因為有重邊,而且請不要將初值賦得太大,這樣會導致在進行動態規划求解的同時溢出,從而導致答案錯誤

  • 如果按照上面那種朴素的做法來進行求解復雜度有可能不能承受,觀察發現我們枚舉了許多不必要的子集,所以我們可以換一個方式:

    for(int i=S;i;i=(i-1)&S)
    

    這樣的話所有的 \(i\) 就一定是 \(S\) 的子集

    蒟蒻的理解:不等於 \(S\)\(S\) 的子集一定在 \([0,S)\)

    然后或運算可以求出在這當中十進制下數字最大的子集 ,設其為 \(P\),然后其余所有的十進制表示比他小的子集都在\([0,P)\) 當中,如此循環求解,自然能夠得到所有的子集

  • 關於狀態的一點點優化

    容易發現,當樹高為 \(i\) 時,至少需要 \(i\) 個節點,所以所有狀態中點的個數小於 \(i\) 的(即二進制位上 \(1\) 的個數小於 \(i\) 的),全部可以不用枚舉子集,直接跳過,這對時間復雜度又有了進一步的常數優化。 這可以通過預處理得到。

最后貼一下代碼,變量名與上面提到的略有不同

#include<bits/stdc++.h>
using namespace std;
const int maxn=12;
int d[maxn+5][maxn+5];
int g[maxn+5][(1<<maxn)+5];
int f[(1<<maxn)+5][(1<<maxn)+5];
int lg[(1<<maxn)+5];//懶 
int q[(1<<maxn)+5],cnt;
int sum[(1<<maxn)+5];
int main()
{
	memset(g,63,sizeof g);
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	int n,m;
	cin>>n>>m;
	for(int i=0;i<n;++i)
		lg[1<<i]=i;//預處理,因為懶
	for(int i=0;i<12;++i)
		for(int j=0;j<12;++j)
			d[i][j]=1000000;//賦最大值
	for(int i=1;i<=m;++i)
	{
		int a,b,c;
		cin>>a>>b>>c;
		--a,--b,d[a][b]=d[b][a]=min(d[a][b],c); 
	}
	int x,S=(1<<n)-1;//全集定義
	for(int i=1;i<=S;++i)
	{
		x=i;
		while(x) x&=(x-1),++sum[i];
	}//預處理每一個狀態上點的個數
	for(int i=1;i<=S;++i)
	{
		cnt=0;
		for(int j=S^i;j;j=(j-1)&(S^i)) q[++cnt]=j;//由於這樣做子集的順序是從大到小的,不符合DP的順序,所以要逆序 
		for(int j=cnt;j>=1;--j)
		{
			int u=lg[q[j]&-q[j]],e=1000000;
			for(int v=0;v<n;++v)
				if(1<<v&i) e=min(d[u][v],e);
			f[i][q[j]]=f[i][q[j]^(q[j]&-q[j])]+e;
		}
	}
	for(int i=0;i<n;++i) g[1][1<<i]=0;//初始狀態
	for(int i=2;i<=n;++i)
		for(int j=(1<<i)-1;j<=S;++j)//剪枝,這里i的初始狀態跳過了肯定不符合的狀態
		{
			if(sum[j]<i) continue;//剪枝,不滿足直接跳過
			for(int k=j;k;k=(k-1)&j)
				g[i][j]=min(g[i][j],g[i-1][j^k]+f[j^k][k]*(i-1));
		}
		int ans=(1<<30);
	for(int i=1;i<=n;++i) ans=min(ans,g[i][S]);//取最小值
	cout<<ans<<endl;
	return 0;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM