「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\),那么狀態轉移方程即為
其中異或操作在這里是取補集的意思,\(\texttt{dis[i][j]}\) 表示從 \(i\) 這個已選點集加上下一層將要選的 \(j\) 這個點集所需要的最小花費
那我們應該如何完善 \(\texttt{dis}\) 數組呢
先給出遞推式
其中 \(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;
}