【网络流24题】 4. 魔术球问题 题解
题意
假设有\(n\)根柱子,现要按下述规则在这\(n\)根柱子中依次放入编号为\(1, 2, 3, \cdots\)的球:
- 每次只能在某根柱子的最上面放球。
- 同一根柱子中,任何\(2\)个相邻球的编号之和为完全平方数。
试设计一个算法,计算出在\(n\)根柱子上最多能放多少个球。例如,在\(4\)根柱子上最多可放\(11\)个球。对于给定的\(n\),计算在\(n\)根柱子上最多能放多少个球。
思路
贪心
对于这道题,我很自然地想到了一个贪心策略。首先,很容易发现,这道题柱子的编号其实根本不重要。那么,我们规定,相同条件下放球时,优先放入编号小的柱子。
当我们放入第\(k\)个球时,我们采用如下贪心策略:
- 从\(1\)到\(n\)扫描所有柱子。如果\(k\)与某个柱子顶端的球数字之和为完全平方数,则放入这个柱子;
- 若所有放入过球的柱子都不能放入,则找到下一个没有放入球的柱子放进去;
- 否则无法放入这个球,输出结果,退出。
正确性
若一个球既可以放入某个有球的柱子,也可以放入某个无球的柱子,那么,放入有球的柱子一定更优。因为将\(k\)放入有球的柱子,会出现一个顶端为\(k\)的柱子和一个空柱子(就是没放进去的那个),而放入空柱子则会出现一个顶端为\(k\)的柱子和一个能与\(k\)匹配的柱子,显然前者比后者更优。
不会出现一个数放入两个已经有球的柱子的情况。因为投入球的顺序一定是从\(1\)到\(n\)逐个递增的,按照上述投入球的规则,最大的若干个球一定都在各个最外侧,而且它们的数一定是连续的。(可以按照上述规则手推一下\(n = 4\)的情况)
网络流
其实个人感觉归为网络流不是很合适,因为把本题抽象成图的做法,本质上是做二分图匹配。关于图的点数的通项,并不是我自己想出来的,也无法给出详细的证明,所以建议优先参考贪心做法。
给出结论:若一共有\(n\)个柱子,则总共能放入\(\left\lfloor \dfrac{n \cdot (n + 2) + (n \bmod 2) - 2}{2} \right\rfloor\)个球。
然后,我们很容易想到,我们尽可能把临近的能组成完全平方数的数穿起来,比如\(1 \rightarrow 3 \rightarrow 6\),那么,一个这样的串上的数就能都放到一个柱子上。同时,我们希望这样的串尽可能少。
有没有觉得很熟悉?希望串尽可能少,剩下的部分就和第三题最小路径覆盖问题一样了。
然后,我们能推算出一共有多少球,这样,我们就能构图了,剩下的就同第三题——最小路径覆盖问题一样了。
代码
贪心
/**
* luogu P2765 https://www.luogu.com.cn/problem/P2765
* 贪心
**/
#include <cstdio>
#include <cstring>
#include <vector>
#include <cmath>
#include <algorithm>
using namespace std;
const int maxn = 10000;
vector<int> s[maxn];
int n, ans, d;
int check(const int &x) { // 判断球x能否放入,能放入则返回放入的柱子编号,否则返回0
for (int i = 1; i <= n; i++) {
if (s[i].empty()) return i;
int t = (int)sqrt(s[i].back() + x);
if (t * t == (s[i].back() + x))
return i;
}
return 0;
}
int main() {
scanf("%d", &n);
while ((d = check(ans + 1))) {
if (d == 0) break;
ans++;
s[d].push_back(ans);
}
printf("%d\n", ans);
for (int i = 1; i <= n; i++) {
for (auto j : s[i])
printf("%d ", j);
putchar('\n');
}
return 0;
}
网络流
/**
* luogu P2765 https://www.luogu.com.cn/problem/P2765
* 网络流
**/
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int maxn = 5000;
const int maxm = 1e5 + 5;
const int INF = 0x3f3f3f3f;
const int S = 0;
const int T = maxn - 1;
struct Edge {
int to, nxt, val;
}e[maxm];
int numedge, head[maxn], n, pre[maxn], nxt[maxn], depth[maxn];
bool ispfs[maxn];
inline void AddEdge(int from, int to, int val) {
e[numedge].to = to;
e[numedge].val = val;
e[numedge].nxt = head[from];
head[from] = numedge;
numedge++;
}
inline bool bfs() {
memset(depth, 0, sizeof(depth));
depth[S] = 1;
queue<int> q;
q.push(S);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = head[u]; ~i; i = e[i].nxt) {
int to = e[i].to;
if (!depth[to] && e[i].val > 0) {
depth[to] = depth[u] + 1;
q.push(to);
}
}
}
return depth[T] != 0;
}
inline int dfs(int u, int flow) {
if (u == T) return flow;
for (int i = head[u]; ~i; i = e[i].nxt) {
int to = e[i].to;
if (depth[to] > depth[u] && e[i].val > 0) {
int di = dfs(to, min(flow, e[i].val));
if (di > 0) {
if (to > n) {
pre[to - n] = u;
nxt[u] = to - n;
}
e[i].val -= di;
e[i ^ 1].val += di;
return di;
}
}
}
return 0;
}
int Dinic() {
int res = 0;
while (bfs()) {
int d = 0;
while ((d = dfs(S, INF))) {
res += d;
}
}
return res;
}
int main() {
memset(head, -1, sizeof(head));
for (int i = 1; i * i < maxn; i++)
ispfs[i * i] = true; // 用来判断i是否是完全平方数
scanf("%d", &n);
n = (n * (n + 2) + (n & 1) - 2) / 2; // 很迷的通项公式
for (int i = 1; i <= n; i++) pre[i] = nxt[i] = i;
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++) {
if (ispfs[i + j]) {
AddEdge(i, j + n, 1);
AddEdge(j + n, i, 0);
}
}
}
for (int i = 1; i <= n; i++) {
AddEdge(S, i, 1);
AddEdge(i, S, 0);
AddEdge(i + n, T, 1);
AddEdge(T, i + n, 0);
}
Dinic();
printf("%d\n", n);
for (int i = 1; i <= n; i++) {
if (pre[i] == i) {
int u = i;
for (u = i; nxt[u] != u; u = nxt[u])
printf("%d ", u);
printf("%d\n", u);
}
}
return 0;
}