放置棋子 / 芯片难题 Chips Challenge
题目链接:ybt金牌导航3-4-4 / luogu UVA1104
题目大意
有一个 n*n 的矩阵,有一个些格子能放东西,有一些不能放,有些一定要放。
要你往尽可能多的地方放上东西,使得满足以下条件:
第 i 行和第 j 列上有东西的格子数相同。
每一行每一列的东西不能超过全部东西的 a/b。(a,b 给出)
输出你最多能再放多少东西(不算一定要放的),如果怎样都无法满足也要判断出来。
思路
看到范围,我们尝试用网络流来做。
我们发现这个超过全部东西的非常的难搞,我们再看用网络流做复杂度还很小,我们考虑直接枚举每一行每一列的东西最多不能超过的值。然后算出总共放多上东西之后再跑到是否满足,在满足的中去放东西最多的那个。
那接着就是怎么算最多放上的东西了。
考虑把放的个数看做是费用,然后选的点的个数是流量,然后跑最大费用最大流。
看到有关行列个数,我们考虑把行和列看成点。
那每行每列有选的限制(你枚举的
n
o
w
now
now),那就把源点连向行,流量是
n
o
w
now
now,费用是
0
0
0。
同理,列连向汇点,流量是
n
o
w
now
now,费用是
0
0
0。
那接着就是可以放的点和一定要放的点了。
那如果一个点
(
a
,
b
)
(a,b)
(a,b) 可以放,那我们就从第
a
a
a 行连向第
b
b
b 列,费用是
1
1
1,流量是
1
1
1。
但是你还要保证一定要放的点一定会被流,那我们考虑用这么一种方法:
如果这个点
(
c
,
d
)
(c,d)
(c,d) 一定要放,那我们就从第
c
c
c 行连向第
d
d
d 列,费用是一个很大的数(一定要超过矩阵点的数量),流量是
1
1
1。
那我们想,我们把算出来的费用除以这个很大的数(整除),得到的就是你选了多少个一定要选的点。
那比它对这个很大的数取模,得到的就是可选可不选的点你选了多少个。
而且因为一定要放的点的费用比可放可不放的点费用大,我们是求最大费用最大流,那两条边只能流一个的情况下,它一定会流一定要放的点的边。
那我们就按这样建图,跑完之后先判断一下是不是所有一定要选的点都被选了(看个数是否符合)。
然后我们来解决下一个问题,如果让第
i
i
i 行第
i
i
i 列选的个数一样?
观察到它们到源点 / 汇点的边的流量都是
n
o
w
now
now,那它们如果选的个数一样,那流的量就一样(假设是
x
x
x),那它们的残余流量就都是
n
o
w
−
x
now-x
now−x。
那我们可以最后搞完判断,但你会发现它如果有多种做法,你无法保证你这个流的做法没有合法就没有别的办法合法。
那我们考虑在建图的时候搞搞。
看到残余流量相同,我们其实可以用一个办法把它们流掉。怎么搞呢?第
i
i
i 行向第
i
i
i 列连一条边,流量是无限大,费用是
0
0
0。
那因为它的费用小于可以放的点和一定要放的点对应的边的费用,那它一定是两个都选不了的时候再选的。
那你就会把两个残余流量都抵消掉,那你只用最后判断是不是从源点出发的边和到汇点的边都流完了就可以。
那做法其实就出来了。
枚举每行 / 列最多的个数,建图,跑最大费用最大流,判断是否流完,判断是否所有必选点都被选,判断是否满足每行 / 列与全部的个数关系。在满足的里面找选的个数最多的就是答案。
当然如果没有满足过就是无论如何都无法满足。
代码
#include<queue>
#include<cstdio>
#include<cstring>
#include<iostream>
#define div 100000
#define INF 0x3f3f3f3f3f3f3f3f
using namespace std;
struct node {
int x, w, to, nxt, op;
}e[100001];
int n, qq[101][101], a, b, tot_num, tmp;
int le[1001], lee[1001], S, T;
int KK, dis[1001], deg[1001];
int water, ans, nowans, alr;
bool in[1001], can;
queue <int> q;
char c;
void csh() {
KK = 0;
memset(le, 0, sizeof(le));
nowans = 0;
}
void add(int x, int y, int z, int w) {
e[++KK] = (node){z, w, y, le[x], KK + 1}; le[x] = KK;
e[++KK] = (node){0, -w, x, le[y], KK - 1}; le[y] = KK;
}
bool SPFA() {//SPFA 跑的是最长路!
for (int i = 0; i <= tot_num; i++) {
dis[i] = INF;
in[i] = 0;
lee[i] = le[i];
deg[i] = -1;
}
while (!q.empty()) q.pop();
q.push(S);
in[S] = 1;
deg[S] = 0;
dis[S] = 0;
while (!q.empty()) {
int now = q.front();
q.pop();
for (int i = le[now]; i; i = e[i].nxt)
if (e[i].x > 0 && dis[e[i].to] > dis[now] + e[i].w) {
dis[e[i].to] = dis[now] + e[i].w;
deg[e[i].to] = deg[now] + 1;
if (!in[e[i].to]) {
in[e[i].to] = 1;
q.push(e[i].to);
}
}
in[now] = 0;
}
return dis[T] != dis[0];
}
int dfs(int now, int sum) {
if (now == T) return sum;
int go = 0;
in[now] = 1;
for (int &i = lee[now]; i; i = e[i].nxt)
if (e[i].x > 0 && dis[e[i].to] == dis[now] + e[i].w && deg[e[i].to] == deg[now] + 1 && !in[e[i].to]) {
int this_go = dfs(e[i].to, min(sum - go, e[i].x));
if (this_go) {
e[i].x -= this_go;
e[e[i].op].x += this_go;
go += this_go;
if (go == sum) return go;
}
}
if (go < sum) deg[now] = -1;
in[now] = 0;
return go;
}
int dinic() {
int re = 0, x;
while (SPFA()) {
x = dfs(S, INF);
while (x) {
re += x;
nowans += x * (-1) * dis[T];
x = dfs(S, INF);
}
}
return re;
}
int main() {
scanf("%d %d %d", &n, &a, &b);
while (n || a || b) {
alr = 0;
ans = -1;
S = n + n + 1;
T = n + n + 2;
tot_num = T;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++) {
c = getchar();
while (c != '/' && c != '.' && c != 'C') c = getchar();
if (c == 'C') qq[i][j] = 2, alr++;
if (c == '.') qq[i][j] = 1;
if (c == '/') qq[i][j] = 0;
}
for (int now = 0; now <= n; now++) {//枚举每行每列最多能放多少东西
csh();
//建图
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
if (qq[i][j] == 1) add(i, n + j, 1, 1 * (-1));
else if (qq[i][j] == 2) add(i, n + j, 1, div * (-1));
for (int i = 1; i <= n; i++) {
add(S, i, now, 0);
add(i + n, T, now, 0);
add(i, i + n, INF, 0);
}
water = dinic();
//看是不是全部的流满(流满才说明这一行和这一列选的个数是一样的)
can = 1;
for (int i = le[S]; i; i = e[i].nxt)
if (e[i].to >= 1 && e[i].to <= n)
if (e[i].x) {
can = 0;
break;
}
if (!can) continue;
for (int i = le[T]; i; i = e[i].nxt)
if (e[i].to >= n + 1 && e[i].to <= n + n)
if (e[e[i].op].x) {
can = 0;
break;
}
if (!can) continue;
if (nowans / div != alr) continue;//一定要选的点没有被选上,不行
int number = nowans / div + nowans % div;
if (1.0 * number * a / b >= 1.0 * now)//要满足每行/列(最多个数那一行/列)的个数不超过全部的比值,判断出没问题才行
ans = max(ans, number - alr);
}
printf("Case %d: ", ++tmp);
if (ans != -1) printf("%d\n", ans);
else printf("impossible\n");
scanf("%d %d %d", &n, &a, &b);
}
return 0;
}