AT1919部門分け
异世界传送门:
洛谷AT1919
ATcoder056C
官方题解
本题解来自
题面
高橋くんのいる会社は \(N\) 人の社員からなる。社員 \(i\) と社員 \(j\) の間には、信頼度 \(w_{i, j}\)
が定まっている。 おかげ様で会社はぐんぐん成長したため、 \(N\) 人をいくつかの部門に分けることになった。ここで、部門分けのスコアを、(部門の数)* \(K\) - (異なる部門に属する 2 人の間の信頼度の総和)と定める。 スコアの最大値を求めるプログラムを書いてください。
\((n \leq 17)\)
一句话题意
\(n\)个人分组,不同组的人之间会产生代价,使 \(组数 * K - 总代价\) 最大化。
这题咋做
考虑到这个题人数很少,我们可以进行状压。
状态\(f[S]\)表示选择了 \(S\)集合时 的状态最大值,从它的子集进行转移,则\[f[S] = max(f[S], K + f[S'] + (新加入的人与子集中的人产生的代价))\]但是如果每次转移的时候\(n^2\)计算新加入的人与子集中的人产生的代价,复杂度是不允许的。
但是我们可以计算该集合内部每个人互相的代价,而他与另一个集合人产生的代价即为两个集合并集的代价减去两个集合内部的代价。
\[设集合A, B, C = A \cup B\] \[f[A, B之间] = f[C] - f[A] - f[B]\]
这样我们可以通过\(O(2^n)\)预处理各子集内部代价,转移时O(1)计算。由于枚举子集\(O(3^n)\), 故整体复杂度\(O(3^n)\), \(3^{17} = 129, 140, 163\), 可以通过此题。
放代码
#include <bits/stdc++.h> namespace fdata { inline char nextchar() { static const int BS = 1 << 21; static char buf[BS], *st, *ed; if (st == ed) ed = buf + fread(st = buf, 1, BS, stdin); return st == ed ? -1 : *st++; } #ifdef lky233 #define nextchar getchar #endif template <typename T> inline T poread() { T ret = 0; char ch; while (!isdigit(ch = nextchar())) ; do ret = ret * 10 + ch - '0'; while (isdigit(ch = nextchar())); return ret; } template <typename Y> inline void poread(Y &ret) { ret = 0; char ch; while (!isdigit(ch = nextchar())) ; do ret = ret * 10 + ch - '0'; while (isdigit(ch = nextchar())); } #undef nextcar } // namespace fdata using fdata::poread; using namespace std; const int MAXN = 18; const long long INF = 1ll << 55; long long w[MAXN][MAXN]; long long memo[1 << MAXN]; long long f[1 << MAXN]; int n; long long k; int main() { #ifdef lky233 freopen("testdata.in", "r", stdin); freopen("testdata.out", "w", stdout); #endif poread(n); poread(k); for (register int i = 0; i < n; ++i) for (register int j = 0; j < n; ++j) poread(w[i][j]); for (register int s = 0; s < (1 << n); ++s) { for (register int i = 0; i < n; ++i) { for (register int j = i + 1; j < n; ++j) { register int si = ((s >> i) & 1); register int sj = ((s >> j) & 1); if (si & sj) memo[s] += w[i][j]; } } } for (register int i = 0; i <= (1 << n); ++i) { for (register int j = i; j; j = (j - 1) & i) { f[i] = max(f[i], k + f[i ^ j] - memo[i] + memo[j] + memo[i ^ j]); } } cout << f[(1 << n) - 1] << endl; }
还有一个小插曲
原题解枚举子集是这么写的
for (register int i = 0; i <= (1 << n); ++i) { register int j = i; do { } while (j != i); }
至于\(j\) \(!=\) \(i\) 为啥能够成功结束循环,当$i = -1 $时,它的二次元二进制表示是\(11111111...\),\(j\)&\(i\) 恰好为 \(j\), 因此成功结束循环。