dfs/状压(没试,参考这个)+ mst。
n
个点的无向图、完全图,让你在其中找一个m
个点的树,满足这个树的边权点权之比在所有m
个点的树中最小。给出这m
个点,若答案不唯一给出字典序最小的点列表。
可以想到,当这m
个点确定了以后,求这m
个点的mst
就行了,所以,问题在于选取哪些m
个点。
主要思想就是dfs
从n
个中选择m
个(
C
n
m
C^m_n
Cnm ),每一个点都有两种选择,O(2^n)复杂度,直到运行到最后一个点,这样形成了一个长度为n
的01序列,若其中选了m
个,那么就运行prim
(优先队列版本)。
这种【n
选m
】的dfs
写法记住就行了(其中有两个剪枝,第一个剪枝是基本的),可以顺便再回顾一下这道题的dfs
写法。
而且这个dfs
写法有讲究的是从1
开始先选再不选,这样可以保证如果存在多个相等答案,字典序最小的那个一定先被赋给ans
。
#include <iostream>
#include <algorithm>
#include <vector>
#include <cstring>
#include <queue>
using namespace std;
const int INF = 1e9;
const int MAXN = 15 + 1;
int N, M;
int weight[MAXN]; // 点权
int g[MAXN][MAXN]; // 边权,完全图
bool select[MAXN]; // 当前选择的 M 个点
bool ans[MAXN]; // 当前最优的 M 个点
double opt;
struct Node
{
int n, d;
bool operator<(const Node& n0) const
{
return d > n0.d;
}
};
void init()
{
opt = INF;
}
double prim() // 求部分点的最小生成树,反正题目是完全图,总能找到边
{
int node_sum = 0;
int edge_sum = 0;
int d[MAXN];
bool vis[MAXN];
priority_queue<Node> q;
bool first = true;
for (int i = 1; i <= N; i++) // 初始化
{
if (select[i])
{
node_sum += weight[i];
d[i] = INF;
vis[i] = false;
if (first)
{
d[i] = 0;
q.push(Node{ i,d[i] });
first = false;
}
}
}
for (; !q.empty();)
{
Node u = q.top();
q.pop();
if (vis[u.n]) continue;
vis[u.n] = true;
edge_sum += u.d;
for (int j = 1; j <= N; j++)
{
if (select[j] && !vis[j] && g[u.n][j] < d[j])
{
d[j] = g[u.n][j];
q.push(Node{ j,d[j] });
}
}
}
return (double)edge_sum / (double)node_sum;
}
void dfs(int v, int num) // 当前已选num个点,判断点v选不选
{ // 从n个里面选m个,每一个选与不选,2^n的复杂度,但这里n就15
if (num > M) return; // 第一个剪枝
if (num + N - v + 1 < M) return; // 第二个剪枝(若之后的都选也达不到m),也可以不加,加上之后下面的 num==M 就可以去掉了
if (v == N + 1) // 递归出口
{
if (num == M)
{
double t = prim();
if (t < opt)
{
opt = t;
memcpy(ans, select, sizeof ans);
}
}
return;
}
select[v] = true; // 这样的展开次序可以保证字典序最小
dfs(v + 1, num + 1);
select[v] = false;
dfs(v + 1, num);
}
int main()
{
for (; ~scanf("%d%d", &N, &M);)
{
if (N == 0 && M == 0) break;
init();
for (int i = 1; i <= N; i++)
scanf("%d", &weight[i]);
for (int i = 1; i <= N; i++)
for (int j = 1; j <= N; j++)
scanf("%d", &g[i][j]);
dfs(1, 0);
bool first = true;
for (int i = 1; i <= N; i++)
{
if (ans[i])
{
if (!first) printf(" ");
printf("%d", i);
first = false;
}
}
printf("\n");
}
return 0;
}
(19年5月25日)上述的代码是走到num > M
处才剪枝,然后最后统一到v == N+1
处得出答案。
下面的代码简单改了一下,还是像二叉树那样展开,不同的是,走到num == M
处就立即运算结果并剪枝;而且,不再用一个长度为n
的01序列来表示,而是用一个vector
来动态地增减元素。
可以想到,每个点都有两个分支(第i
个点最多有2^(i-1)
次调用),每个dfs
过程执行完毕之后select
都是空的(从底向上,按照数学归纳法考虑)。
与这道题的dfs
写法其实有共通之处。
#include <iostream>
#include <algorithm>
#include <vector>
#include <cstring>
#include <queue>
using namespace std;
const int INF = 1e9;
const int MAXN = 15 + 1;
int N, M;
int weight[MAXN]; // 点权
int g[MAXN][MAXN]; // 边权,完全图
vector<int> select;
vector<int> ans;
double opt;
struct Node
{
int n, d;
bool operator<(const Node& n0) const
{
return d > n0.d;
}
};
void init()
{
opt = INF;
//select.clear(); // 没必要。。dfs自动收拾干净了
//ans.clear();
}
double prim() // 求部分点的最小生成树,反正题目是完全图,总能找到边
{
int node_sum = 0;
int edge_sum = 0;
int d[MAXN];
bool vis[MAXN];
priority_queue<Node> q;
bool first = true;
for (int i = 0; i < M; i++) // 初始化
{
int i0 = select[i];
node_sum += weight[i0];
d[i0] = INF;
vis[i0] = false;
if (first)
{
d[i0] = 0;
q.push(Node{ i0,d[i0] });
first = false;
}
}
for (; !q.empty();)
{
Node u = q.top();
q.pop();
if (vis[u.n]) continue;
vis[u.n] = true;
edge_sum += u.d;
for (int j = 0; j < M; j++)
{
int j0 = select[j];
if (!vis[j0] && g[u.n][j0] < d[j0])
{
d[j0] = g[u.n][j0];
q.push(Node{ j0,d[j0] });
}
}
}
return (double)edge_sum / (double)node_sum;
}
void dfs(int v)
{
int num = select.size();
if (num == M) // 在这里v绝对不会>N+1
{
double t = prim();
if (t < opt)
{
opt = t;
ans = select;
}
return;
}
if (num + N - v + 1 < M) return; // 剪枝,而且其作用已经蕴含了下句(递归出口),下句可以删了,因为到这里必然有 num<M
if (v == N + 1) return;
select.push_back(v);
dfs(v + 1);
select.pop_back();
dfs(v + 1);
}
int main()
{
for (; ~scanf("%d%d", &N, &M);)
{
if (N == 0 && M == 0) break;
init();
for (int i = 1; i <= N; i++)
scanf("%d", &weight[i]);
for (int i = 1; i <= N; i++)
for (int j = 1; j <= N; j++)
scanf("%d", &g[i][j]);
dfs(1);
bool first = true;
for (int i = 0; i < M; i++)
{
if (!first) printf(" ");
printf("%d", ans[i]);
first = false;
}
printf("\n");
}
return 0;
}