问题:
参与考古挖掘的小明得到了一份藏宝图,藏宝图上标出了 n 个深埋在地下的宝藏屋, 也给出了这 n 个宝藏屋之间可供开发的 m 条道路和它们的长度。
小明决心亲自前往挖掘所有宝藏屋中的宝藏。但是,每个宝藏屋距离地面都很远, 也就是说,从地面打通一条到某个宝藏屋的道路是很困难的,而开发宝藏屋之间的道路 则相对容易很多。
小明的决心感动了考古挖掘的赞助商,赞助商决定免费赞助他打通一条从地面到某 个宝藏屋的通道,通往哪个宝藏屋则由小明来决定。
在此基础上,小明还需要考虑如何开凿宝藏屋之间的道路。已经开凿出的道路可以 任意通行不消耗代价。每开凿出一条新道路,小明就会与考古队一起挖掘出由该条道路 所能到达的宝藏屋的宝藏。另外,小明不想开发无用道路,即两个已经被挖掘过的宝藏 屋之间的道路无需再开发。
新开发一条道路的代价是:
L×K
L代表这条道路的长度,K代表从赞助商帮你打通的宝藏屋到这条道路起点的宝藏屋所经过的 宝藏屋的数量(包括赞助商帮你打通的宝藏屋和这条道路起点的宝藏屋) 。
请你编写程序为小明选定由赞助商打通的宝藏屋和之后开凿的道路,使得工程总代 价最小,并输出这个最小值。
解:
细数我做这道题的心历路程
首先我看到n这么小 我就先定义 了 $f[i]$ 表示集合 都遍历的最小花费
$dis[j]$ 表示j的深度 (我开始没看出来要选一棵树,还有深度....)
$dp[i|(1<<j-1)]=min(dp[i|1<<j-1],dis[edge[k].st]*edge[k].val+dp[i])$ 其中终点为不在集合内的j 起点为在集合的st
然后从CCF那里光荣地骗了 50分 ....
原因在于 时间复杂度较高 并且dis数组算的有问题
code:
#include<stdio.h> #include<cstdio> #include<algorithm> #include<cmath> #include<bits/stdc++.h> using namespace std; #define maxn 5555 #define maxnn 5000 #define ll long long ll dp[maxn]; int dis[maxn]; int tot=0; int all; struct node { int st,en; int val; }edge[maxnn]; int n,m; void add(int a,int b,int c) { edge[++tot].en=b; edge[tot].st=a; edge[tot].val=c; } void dfs(int v) { for(int i=0;i<=n;i++) dis[i]=0; for(int i=0;i<=all;i++) { dp[i]=100000000000; } dis[v]=1; dp[1<<v-1]=0; for(int i=1<<v-1;i<=all;i++) { for(int j=1;j<=n;j++) { if(((1<<j-1)&i)==0) { for(int k=1;k<=tot;k++) { if((((1<<edge[k].st-1)&i)!=0)&&(edge[k].en==j)&&(dp[i|1<<j-1]>dis[edge[k].st]*edge[k].val+dp[i])) { dp[i|(1<<j-1)]=min(dp[i|1<<j-1],dis[edge[k].st]*edge[k].val+dp[i]); dis[j]=dis[edge[k].st]+1; } } } } } } int main() { cin>>n>>m; int x,y,z; ll ans=100000000000; all=(1<<n)-1; for(int i=1;i<=m;i++) { scanf("%d%d%d",&x,&y,&z); add(x,y,z); add(y,x,z); } for(int i=1;i<=n;i++) { dfs(i); ans=min(ans,dp[all]); } cout<<ans; }
怎么办?
注意到,我们其实是dis数组算的不对 也就是深度数组算的不对
并且一个点是由一个点的深度值*VAL权值更新而来 且n这么小 肯定是定义二维数组
所以我们有 的$f[i][j]$ 表示 集合i 并且当前讨论到j 层深度的最小花费
状态转移方程也就出来了
$dp[i|k][j]=min(dp[i|k][j],dp[i][j-1]+(j)*sum k^i)$ 其中$sum k^i $ 为深度为j的集合k到集合i的最小距离之和
k是i的补集的子集 也就是包含了没有被讨论过的点
这里我们就要安利一波枚举子集的方法
for(int i=s,i;i=(i-1)&s)
还有一波快速取出集合元素的方法(x就是原来集合的为1的元素)
for(int i=s;i;i=i-(i&-i)) { int x=i&-i; }
据说这样和logg2数组搭配更快哦
但是
在经历了一个多小时的卡常优化后 我还是成功被卡常
luoguAC oj T 75pts 佛了
code:
#include<stdio.h> #include<cstdio> #include<algorithm> #include<cmath> #include<bits/stdc++.h> using namespace std; #define maxn 5555 #define maxnn 5000 #define ll long long ll dp[maxn][maxnn]; int sum[maxnn][maxnn]; int mapp[30][30]; int dis[maxnn][maxnn]; int tot=0; int all; int n,m; int logg[1<<14]; inline void init() { for(int i=0;i<=n;i++) { for(int j=0;j<=all;j++) { dis[i][j]=10000000; } } for(int j=0;j<=all;j++) { int s0=j^all; for(int i=s0;i;i=(i)-(i&-i)) { int x=i&-i; for(int k=j;k;k=k-(k&-k)) { int u=k&-k; { dis[logg[x]][j]=min(dis[logg[x]][j],mapp[logg[x]][logg[u]]); } } } } for(int j=0;j<=all;j++) { int s=j^all; for(int u=s;u;u=((u-1)&s)) for(int i=u;i;i=(i)-(i&-i)) { int x=i&-i; { sum[u][j]+=dis[logg[x]][j]; } } } } int main() { cin>>n>>m; all=(1<<n)-1; for(int j=1;j<=n;j++) { logg[1<<j-1]=j; } for(int j=0;j<=n;j++) { for(int i=0;i<=n;i++) { if(i!=j) mapp[i][j]=100000000; } } int a,b,c; for(int i=1;i<=m;i++) { scanf("%d%d%d",&a,&b,&c); mapp[a][b]=min(mapp[a][b],c); mapp[b][a]=min(mapp[a][b],c); } init(); ll ans=1000000000; for(int i=1;i<=n;i++) { for(int j=0;j<=n;j++) { for(int i=0;i<=all;i++) { dp[i][j]=1000000000; } } dp[1<<i-1][0]=0; for(int i=0;i<=all;i++) { for(int j=1;j<=n;j++) { int s0=(all^i); for(int k=s0;k;k=((k-1)&s0)) { dp[i|k][j]=min(dp[i|k][j],dp[i][j-1]+(j)*sum[k][i]); } } } for(int j=0;j<=n;j++) { ans=min(ans,dp[all][j]); } } printf("%lld",ans); }
我同学帮我玄学优化卡过了
最后我发现了一个神奇的解法
爆搜
注意到 8!的全排列 其实很小 那就爆搜啊
据说当年Oblack学长 就是爆搜 拿了90pts %%%
花费时间:0ms
我们需要枚举根节点 然后依次进行转移回溯
再次证明了爆搜+剪枝的重要性
code:
// #include<bits/stdc++.h> using namespace std; #define maxnn 30 int tar[maxnn][maxnn]; int cnt=0; int n,m; int d[maxnn]; int vis[maxnn]; #define inf 1000000 int tmp=0,tot; int ans=100000000; int c[maxnn][maxnn]; int le[maxnn]; int p; bool cmp(int a,int b){ return c[p][a]<c[p][b]; } void dfs(int num,int node) { for(int i=num;i<=cnt;i++) { if(tot+tmp*le[vis[i]]>ans) return ; for(int j=node;j<=d[vis[i]];j++) { if(!le[tar[vis[i]][j]]) { cnt++; vis[cnt]=tar[vis[i]][j]; tmp-=c[vis[cnt]][tar[vis[cnt]][1]]; tot+=c[vis[i]][vis[cnt]]*le[vis[i]]; le[vis[cnt]]=le[vis[i]]+1; dfs(i,j+1); tot-=c[vis[i]][vis[cnt]]*le[vis[i]]; le[vis[cnt]]=0; tmp+=c[vis[cnt]][tar[vis[cnt]][1]]; cnt--; } } node =1; } if(cnt==n) { if(tot<ans) ans=tot; return ; } } int main() { int x,y,z; scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) { c[i][j]=inf; } for(int i=1;i<=m;i++) { scanf("%d%d%d",&x,&y,&z); if(c[x][y]==inf) tar[x][++d[x]]=y,tar[y][++d[y]]=x; if(c[x][y]>z) { c[x][y]=z; c[y][x]=z; } } for(int i=1;i<=n;i++) { p=i; sort(tar[i]+1,tar[i]+1+d[i],cmp); tmp+=c[i][tar[i][1]]; } for(int i=1;i<=n;i++) { tot=0; cnt=1; vis[1]=i; le[i]=1; tmp-=c[i][tar[i][1]]; dfs(1,1); le[i]=0; tmp+=c[i][tar[i][1]]; } printf("%d",ans); }
summay:
*通过这道题我觉得CCF的题并不简单 考了状压DP和枚举子集 取集合元素等操作
*爆搜并不是不可过 爆搜非常重要 要多练爆搜