2010-1125
从 Trac 迁移的文章
这是从旧校内 Wiki 迁移的文章,可能存在一些样式问题,您可以向 memset0 反馈。
原文章内容如下:
== 题目大意 ==
集训队分学校的奖金的时候,由于学校只把钱发给每个队第一个人,然后又由于每个人可能加入了不同的队伍,所以当刚从学校拿到钱的时候,有的人会多拿,有的人会少拿,有些人之间有一定长度的道路联通。
定义一次传送为一个人通过一个的路径把非负的钱送给另外一个人,无论钱数额多少,此过程花费为路径的长度。
现要求最终传送的结果为每个人都拿到自己应得的数额,求最小化所有传送的花费和。
== 分析 ==
这个题目可以被理解成:
* 一个无向图,有边权(非负)和点权(整数),把这个图划分成几个连通子图,在每一个子图,点权和为 0,最小化 ```Sum{i 的边权利和,{i 是一个划分出的子图} }```
考虑划分出的一个子图,如果其中的点权和为 0,那么它的传送花费就是这个子图的最小生成树。
于是问题转换成把原图划分成几个点权和为 0 的连通子图,最小化它们的 MST 和。
== 算法 ==
用 f(bit_mask),bit_mask 按位选取了一些点(人),来表示:
* 将这些人分成一个子图(不排除进一步划分),所需传送的最小花费。
那么最终解就是 f((1 << n) -1)。
f(x) 有意义仅当 x 选取的点权和为 0 并且这些点是连通的。
对于有意义的 f(x),可以将 x 进一步划分为 ```x1 | x2```,其中 ```(x1 & x2) == 0```,返回 ```min{f(x1) + f(x2), MST(nodes selected by x)}``` 。枚举这个划分复杂度 ```O(popcount(x)^3)```
f(x) 满足全局最优子问题也最优,并且划分没有后效性,可以记忆化。
== 参考程序 ==
{{{
#!cpp
#include <cassert>
#include <cstdio>
#include <iostream>
#include <bitset>
#include <map>
#include <vector>
#include <algorithm>
#include <cstring>
using namespace std;
namespace debug {
static const bool DEBUG = false;
#define D if(debug::DEBUG)
static int _callstack_depth;
template <class R>
class _scoped_callstack_info {
const R* pr;
void indent() {
D for (int i = 0; i < _callstack_depth * 2; i++) cout << ' ';
}
public:
_scoped_callstack_info(const R& r = NULL) {
D {
indent();
pr = &r;
_callstack_depth++;
}
}
~_scoped_callstack_info() {
D {
_callstack_depth--;
if (pr) indent(), cout << "res = " << *pr << endl;
}
}
};
#define CALLSTACK_INFO(v, r) \
debug::_scoped_callstack_info<__typeof(r)> _callstack_info(r);\
D std::cout << __func__ << "(" << v << ")" << std::endl;
}
const int N = 16;
const int M = 128;
int n, m;
int v[N];
int fc[1 << N];
map<int, int> l2p;
#ifndef ONLINE_JUDGE
__attribute__((__constructor__))
#endif
void l2p_init() {
int v = 1;
for (int i = 0; i < N; i++) {
l2p[v] = i;
v <<= 1;
}
}
template<int n>
struct DisjointSet {
int p[n];
DisjointSet() {
for (int i = 0; i < n; i++) p[i] = i;
}
inline int find(int x) {
if (p[x] == x) return x; else return p[x] = find(p[x]);
}
inline void join(int x, int y) {
p[find(x)] = find(y);
}
inline bool isjoint(int x, int y) {
return find(x) == find(y);
}
};
struct Edge;
Edge* em[N][N];
struct Edge {
int p[2];
int w;
void read() {
scanf("%d%d%d", &p[0], &p[1], &w);
// fill in matrix
em[p[0]][p[1]] = em[p[1]][p[0]] = this;
}
bool operator < (const Edge& rhs) const {
return w < rhs.w;
}
} e[M], etmp[M];
int etmpc;
int ids[N], idsc = 0;
int f(int mask) {
if (fc[mask] == -1) {
int r = -2;
// CALLSTACK_INFO("mask: " << mask, r);
{
// judge if these points sum up equals zero
int sum = idsc = 0;
int t = mask;
while (t != 0) {
sum += v[ids[idsc] = l2p.find(t & (-t))->second];
// D printf("ids[%d] = %d\n", idsc, ids[idsc]);
t = t & (t - 1);
idsc ++;
}
D printf(" sum = %d\n", sum);
// sum != zero, invalid cut
if (sum != 0) { r = -3; goto ret; }
// judge connectivity (M) (combined in calculating MST)
// if (!connected(mask, mask & (-t))) return -3;
// collect edges and do mst ... O(? mask * N^2)
etmpc = 0;
for (int i = 0; i < idsc; i++) {
for (int j = i + 1; j < idsc; j++) {
// D printf(" i, j = %d, %d | %p\n", i, j, em[ids[i]][ids[j]]);
if (em[ids[i]][ids[j]] != NULL) {
etmp[etmpc++] = *em[ids[i]][ids[j]];
}
}
}
sort(etmp, etmp + etmpc);
int joint_w = 0, joint_n = 0;
{
DisjointSet<N> d;
for (int i = 0; i < etmpc; i++) {
// try add edge etmp[i]
Edge& edge = etmp[i];
if (!d.isjoint(edge.p[0], edge.p[1])) {
// add that edge
d.join(edge.p[0], edge.p[1]);
joint_n++;
joint_w += edge.w;
// D printf("add edge.w = %d\n", edge.w);
if (joint_n == idsc - 1) {
// ok
break;
}
}
}
}
// D printf("j_n = %d, j_w = %d\n", joint_n, joint_w);
if (joint_n == idsc - 1) r = joint_w; else r = -4;
// cut to 2 pieces, and get result
for (int sub = (mask - 1) & mask; sub > 0; sub = (sub - 1) & mask) {
int incsub = ~sub & mask;
// if (sub > incsub) break;
int r1 = f(sub), r2;
if (r1 >= 0) {
r2 = f(incsub);
if (r2 >= 0) {
r1 += r2;
if (r < 0 || r1 < r) r = r1;
}
}
}
}
ret:
fc[mask] = r;
}
return fc[mask];
}
int main(int argc, char const* argv[]) {
#ifdef ONLINE_JUDGE
l2p_init();
#endif
while (scanf("%d%d", &n, &m) == 2) {
D puts("========================");
assert(n >= 2 and n <= N);
assert(m >= 0 and m <= n * (n - 1) / 2);
memset(fc, -1, sizeof(fc));
memset(em, 0, sizeof(em));
for (int i = 0; i < n; i++) scanf("%d", &v[i]);
for (int i = 0; i < m; i++) e[i].read();
int r = f((1 << n) - 1);
if (r < 0) puts("Impossible");
else printf("%d\n", r);
}
return 0;
}
}}}
题目大意
集训队分学校的奖金的时候,由于学校只把钱发给每个队第一个人,然后又由于每个人可能加入了不同的队伍,所以当刚从学校拿到钱的时候,有的人会多拿,有的人会少拿,有些人之间有一定长度的道路联通。
定义一次传送为一个人通过一个的路径把非负的钱送给另外一个人,无论钱数额多少,此过程花费为路径的长度。
现要求最终传送的结果为每个人都拿到自己应得的数额,求最小化所有传送的花费和。
分析
这个题目可以被理解成:
- 一个无向图,有边权(非负)和点权(整数),把这个图划分成几个连通子图,在每一个子图,点权和为 0,最小化 ``
Sum{i 的边权利和,{i 是一个划分出的子图} }``
考虑划分出的一个子图,如果其中的点权和为 0,那么它的传送花费就是这个子图的最小生成树。
于是问题转换成把原图划分成几个点权和为 0 的连通子图,最小化它们的 MST 和。
算法
用 f(bit_mask),bit_mask 按位选取了一些点(人),来表示:
- 将这些人分成一个子图(不排除进一步划分),所需传送的最小花费。
那么最终解就是 f((1 << n) -1)。
f(x) 有意义仅当 x 选取的点权和为 0 并且这些点是连通的。
对于有意义的 f(x),可以将 x 进一步划分为 ``x1 | x2`,其中 `(x1 & x2) == 0`,返回 `min{f(x1) + f(x2), MST(nodes selected by x)}` 。枚举这个划分复杂度 `O(popcount(x)^3)``
f(x) 满足全局最优子问题也最优,并且划分没有后效性,可以记忆化。
参考程序
#include <cassert>
#include <cstdio>
#include <iostream>
#include <bitset>
#include <map>
#include <vector>
#include <algorithm>
#include <cstring>
using namespace std;
namespace debug {
static const bool DEBUG = false;
#define D if(debug::DEBUG)
static int _callstack_depth;
template <class R>
class _scoped_callstack_info {
const R* pr;
void indent() {
D for (int i = 0; i < _callstack_depth * 2; i++) cout << ' ';
}
public:
_scoped_callstack_info(const R& r = NULL) {
D {
indent();
pr = &r;
_callstack_depth++;
}
}
~_scoped_callstack_info() {
D {
_callstack_depth--;
if (pr) indent(), cout << "res = " << *pr << endl;
}
}
};
#define CALLSTACK_INFO(v, r) \
debug::_scoped_callstack_info<__typeof(r)> _callstack_info(r);\
D std::cout << __func__ << "(" << v << ")" << std::endl;
}
const int N = 16;
const int M = 128;
int n, m;
int v[N];
int fc[1 << N];
map<int, int> l2p;
#ifndef ONLINE_JUDGE
__attribute__((__constructor__))
#endif
void l2p_init() {
int v = 1;
for (int i = 0; i < N; i++) {
l2p[v] = i;
v <<= 1;
}
}
template<int n>
struct DisjointSet {
int p[n];
DisjointSet() {
for (int i = 0; i < n; i++) p[i] = i;
}
inline int find(int x) {
if (p[x] == x) return x; else return p[x] = find(p[x]);
}
inline void join(int x, int y) {
p[find(x)] = find(y);
}
inline bool isjoint(int x, int y) {
return find(x) == find(y);
}
};
struct Edge;
Edge* em[N][N];
struct Edge {
int p[2];
int w;
void read() {
scanf("%d%d%d", &p[0], &p[1], &w);
// fill in matrix
em[p[0]][p[1]] = em[p[1]][p[0]] = this;
}
bool operator < (const Edge& rhs) const {
return w < rhs.w;
}
} e[M], etmp[M];
int etmpc;
int ids[N], idsc = 0;
int f(int mask) {
if (fc[mask] == -1) {
int r = -2;
// CALLSTACK_INFO("mask: " << mask, r);
{
// judge if these points sum up equals zero
int sum = idsc = 0;
int t = mask;
while (t != 0) {
sum += v[ids[idsc] = l2p.find(t & (-t))->second];
// D printf("ids[%d] = %d\n", idsc, ids[idsc]);
t = t & (t - 1);
idsc ++;
}
D printf(" sum = %d\n", sum);
// sum != zero, invalid cut
if (sum != 0) { r = -3; goto ret; }
// judge connectivity (M) (combined in calculating MST)
// if (!connected(mask, mask & (-t))) return -3;
// collect edges and do mst ... O(? mask * N^2)
etmpc = 0;
for (int i = 0; i < idsc; i++) {
for (int j = i + 1; j < idsc; j++) {
// D printf(" i, j = %d, %d | %p\n", i, j, em[ids[i]][ids[j]]);
if (em[ids[i]][ids[j]] != NULL) {
etmp[etmpc++] = *em[ids[i]][ids[j]];
}
}
}
sort(etmp, etmp + etmpc);
int joint_w = 0, joint_n = 0;
{
DisjointSet<N> d;
for (int i = 0; i < etmpc; i++) {
// try add edge etmp[i]
Edge& edge = etmp[i];
if (!d.isjoint(edge.p[0], edge.p[1])) {
// add that edge
d.join(edge.p[0], edge.p[1]);
joint_n++;
joint_w += edge.w;
// D printf("add edge.w = %d\n", edge.w);
if (joint_n == idsc - 1) {
// ok
break;
}
}
}
}
// D printf("j_n = %d, j_w = %d\n", joint_n, joint_w);
if (joint_n == idsc - 1) r = joint_w; else r = -4;
// cut to 2 pieces, and get result
for (int sub = (mask - 1) & mask; sub > 0; sub = (sub - 1) & mask) {
int incsub = ~sub & mask;
// if (sub > incsub) break;
int r1 = f(sub), r2;
if (r1 >= 0) {
r2 = f(incsub);
if (r2 >= 0) {
r1 += r2;
if (r < 0 || r1 < r) r = r1;
}
}
}
}
ret:
fc[mask] = r;
}
return fc[mask];
}
int main(int argc, char const* argv[]) {
#ifdef ONLINE_JUDGE
l2p_init();
#endif
while (scanf("%d%d", &n, &m) == 2) {
D puts("========================");
assert(n >= 2 and n <= N);
assert(m >= 0 and m <= n * (n - 1) / 2);
memset(fc, -1, sizeof(fc));
memset(em, 0, sizeof(em));
for (int i = 0; i < n; i++) scanf("%d", &v[i]);
for (int i = 0; i < m; i++) e[i].read();
int r = f((1 << n) - 1);
if (r < 0) puts("Impossible");
else printf("%d\n", r);
}
return 0;
}