提示:
1. 看到题目 , 最短路? 最小费用流?(博主才做完图论回来做这道题的第一反应) ... 可能你会想到枚举对应关系然后写k个最短路 ,但是可能存在一些点彼此联通 , 共用一些边 导致独立求最短路的情形是错误的。 我们不妨先将问题弱化一下 , 如果求这些点(前K个 和 后K个)彼此都联通, 你想到了什么?
2. 看完提示1 没有思路说明你和当时博主一样不熟悉一个根据动态规划为原理 , 广泛应用于组合的囊括最小生成树的一种树 , 斯坦纳树
3. 斯坦纳树算法就能求使一个给定点集内点彼此联通的最小代价(所包含的边可以是连接这个点集以外的点) , 如果用提示1.中的弱化问题 , 这就是一个裸的斯坦纳树题目(其方法会在之后介绍)
4. 假设你能够求出给定点集的斯坦纳树 , 你还是无法想出此题的思路往下看。 本题并不要求所有点联通 , 但至少两两联通 , 那么可以把答案看成几颗斯坦纳树 , 你的任务就是枚举这些树的可能形态 , 后来你会发现 , 那也是个动态规划 (dp是思想 , 不是具体方法)
5. 斯坦纳树网上有很多文章 , 可以到网上搜详细讲解 , 这里只给出dp状态转移方程和评价:
d [i] [s] : 以i 为树根 , 包含集合s中所有点(s用二进制表示 , 并且s仅是所选点集中的点) , 不难发现状态数为 n*(2^k) // n为节点数 , k 为所选点的个数
这里强调一下dp中一个重要概念 , 状态的顺序 , 如果两个状态a , b , a的dp值可以影响b的dp值 ,但是b的dp值不能影响a的dp值 , 简单点说就是只能用a推出b , 那么我们就说a的状态比b的状态靠前 , 但如果a,b可以互相影响 , 那么我们就说这两个状态是同层的 (同层dp , 见过吗 , 刘汝佳紫书上有一题)
斯坦纳树就是一个需要考虑同层dp的问题 , 于是 dp 的状态转移方程分成两个部分:
从前往后推: d [ i ] [ s ] = min( d [ i ] [s'] + d [ i ] [ s-s' ] ) // 其中s'为s的子集 , 所以说是从前往后推
同层dp : d [ i ] [ s ] = d [ j ] [ s ] + dis [ i ] [ j ] // 其中dis[i][j]为 i 到 j 的最短路 , 这个可以用floyd预处理 , 当然也可以现场写dijkstra 或者 bellmanFord(SPFA)
当然答案就是min( d [ i ] [ 所选点集的全集 ] )
后面提到的计算答案的动态规划看代码 , 注意斯坦纳树只需要求一次就可以了
博主的疑问: 同层dp一般来说是需要写dijkstra等最短路算法的 , 但是此题直接更新也没有问题 ,博主这里有些疑惑, 如果读者能解答 , 望告知博主(QQ:812483101) ,
Update: 由于博主没有写主流的SteinerTree的版本, 其实所谓的SPFA就是同层转移 , 可以网上BZOJ4006的代码和讲解来看看主流的SteinerTree的写法。
注意: 用addEdge初始化数组 f 的时候一定要加min , 博主被坑过
// // main.cpp // UVa1496 // // Created by Fuxey on 15/10/19. // Copyright © 2015年 corn.crimsonresearch. All rights reserved. // #include <iostream> #include <cstdio> #include <cstdlib> #include <cstring> #include <cmath> #include <vector> #include <deque> #include <string> #include <stack> #include <algorithm> using namespace std; const int maxn = 60; const int maxk = 7*2; const int INF = 1<<29; struct edge { int t , v; edge(int t=0 ,int v=0):t(t),v(v){} }; struct StreinerTree { int n; int f[maxn][maxn] , d[maxn][1<<maxk]; int book[maxn]; void init(int n) { this->n = n; for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) f[i][j] = INF; } void addEdge(int ff , int t , int v) { f[ff][t] = min(v , f[ff][t]); f[t][ff] = min(v , f[t][ff]); } void floyd() { for(int k=1;k<=n;k++) for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) f[i][j] = min(f[i][j] , f[i][k]+f[k][j]); } void mincostST(vector<int> s) { for(int i=1;i<=n;i++) for(int j=0;j<(1<<s.size());j++) d[i][j] = INF; for(int i=0;i<s.size();i++) d[s[i]][1<<i] = 0; for(int i=1;i<(1<<s.size());i++) { for(int j=1;j<=n;j++) for(int ii = (i-1)&i;ii>0;ii = (ii-1)&i) d[j][i] = min(d[j][i] , d[j][ii]+d[j][i-ii]); for(int j=1;j<=n;j++) for(int k=1;k<=n;k++) d[j][i] = min(d[j][i] , d[k][i]+f[j][k]); /* memset(book, 0, sizeof(book)); for(int j=1;j<=n;j++) { int Min = INF , x = 0; for(int k=1;k<=n;k++) if(Min >= d[k][i] && book[k]==0) Min = d[k][i] , x = k; book[x] = 1; for(int k=1;k<=n;k++) d[k][i] = min(d[k][i] , d[x][i]+f[x][k]); } */ // 这里就是那个n^2的dijkstra的更新方法 , 博主认为这样会更可靠一些 } } }solver; int ans[1<<(maxk/2)][1<<(maxk/2)]; int main(int argc, const char * argv[]) { ios::sync_with_stdio(false); int t; cin>>t; while(t--) { int n , m , K; cin>>n>>m>>K; solver.init(n); while(m--) { int a , b , c; cin>>a>>b>>c; solver.addEdge(a, b, c); } solver.floyd(); vector<int> s; for(int i=1;i<=K;i++) s.push_back(i); for(int i=n-K+1;i<=n;i++) s.push_back(i); solver.mincostST(s); for(int s1 = 1;s1<(1<<K);s1++) for(int s2 = 1;s2<(1<<K);s2++) if(__builtin_popcount(s1) == __builtin_popcount(s2)) { int now = s1+(1<<K)*s2; ans[s1][s2] = INF; for(int i=1;i<=n;i++) ans[s1][s2] = min(ans[s1][s2] , solver.d[i][now]); for(int i=(s1-1)&s1;i>0;i = (i-1)&s1) for(int j=(s2-1)&s2;j>0;j = (j-1)&s2) if(__builtin_popcount(i) == __builtin_popcount(j)) ans[s1][s2] = min(ans[s1][s2] , ans[i][j]+ans[s1-i][s2-j]); } if(ans[(1<<K)-1][(1<<K)-1]==INF) cout<<"No solution\n"; else cout<<ans[(1<<K)-1][(1<<K)-1]<<endl; } return 0; }