【ybt金牌导航3-2-4】【luogu P5038】奇怪游戏 / 奇怪的游戏

奇怪游戏 / 奇怪的游戏

题目链接:ybt金牌导航3-2-4 / luogu P5038

题目大意

有一个棋盘,每次你可以选相邻的两个位置都加一。
问你最少要多少次操作才能让棋盘上的数都变成一样的,如果不能就输出 -1。

思路

看到相邻的位置想到把图黑白染色。
然后要把数变成一样你就可以先到用网络流来搞,判断它是否流满的方法来看是否变成一样。
但它要的是最少次的操作,网络流是求最大流,自然想到用二分。

然后开始搞具体实现。
首先你二分的肯定是最后变成的数 \(X\),那黑色点的个数是 \(num_0\),权值和是 \(sum_0\),白色的个数是 \(num_1\),权值和是 \(sum_1\),那我们就是要找一个最小的 \(X\),使得这个式子被满足:
\(num_0*X-sum_0=num_1*X-sum_1\)(因为你操作一次就相当于给 \(sum_0,sum_1\) 都加一)

然后化一下式子,得到:\(X=\dfrac{sum_0-sum_1}{num_0-num_1}\)
那如果 \(num_0=num_1\),那除数就变成了 \(0\),那就出问题了,这个时候是什么鬼呢?

我们考虑从 \(x*y=z\),得到 \(x=\frac{z}{y}\),那当 \(y=0,z=0\) 时,\(x\) 可以是任意的数,因为任意的数乘 \(0\) 都是 \(0\)
那也就是说,当 \(sum_0=sum_1\)\(num_0=num_1\) 时,就会有多组解,那这个时候我们就要上二分,通过用网络流来验证这个答案行不行,然后找到最小的那个。
那如果 \(y=0,z\neq0\),就是 \(x\) 什么实数都不行,因为没有实数乘 \(0\) 会变成非零数。那也就说当 \(num_0=num_1\)\(sum_0\neq sum_1\) 的时候,就是无解的,直接输出 \(-1\)

那如果 \(num_0\neq num_1\),那你会发现,这个 \(X\) 的解是唯一的,而且你还能把它算出来。
但是你算出来之后你还要用网络流验算一下,如果不是还是要输出 \(-1\)

那接着新的问题又来了,如果构网络流的图(是跑最大流应该很显然了吧)。
那我们考虑这样,假设你二分的是 \(X\),点 \((i,j)\) 权值为 \(a_{i,j}\)
源点连线黑色点 \((x_1,y_1)\),流量是 \(X-a_{x_1,y_1}\)
表示这个黑点最多能被加多少次。
那现在只有黑色点能被加,我们就把黑色点连向它旁边的白色点,流量无限。
这样就实现了黑点和白点捆绑加,你加黑点就一定要把其中一个旁边的白点也加了,不然不能流到白点。
那白点又能被加多少次呢,白色 \((x_2,y_2)\) 连汇点,流量是 \(X-a_{x_2,y_2}\)。(流量就是它能被加的次数)

那如果判断这个方案是否可行呢?
我们可以跑最大流,然后看是否已经流满,就看最大流是否等于所有 \(X-a_{x_1,y_1}\) 的和。
(当然你用 \(X-a_{x_2,y_2}\) 也可以,反正你求出来的公式就已经让它们是相同的)

然后搞就可以了。

代码

#include<queue>
#include<cstdio>
#include<cstring>
#include<iostream>
#define ll long long
#define INF 0x3f3f3f3f3f3f3f3f

using namespace std;

struct node {
	ll x, to, nxt, op;
}e[500001];
ll TI, n, m, le[5001], lee[5001], S, T;
ll a[41][41], l, r, KK, dis[5001], tot_num;
ll dx[4] = {1, 0, -1, 0}, dy[4] = {0, 1, 0, -1};
ll sum[2], num[2];
queue <int> q;

void csh() {
	l = 0;
	r = INF;
	sum[0] = sum[1] = 0;
	num[0] = num[1] = 0;
}

void csh_wll() {
	memset(le, 0, sizeof(le));
	KK = 0;
	tot_num = 0; 
}

bool ck(ll x, ll y) {
	if (x < 1 || x > n) return 0;
	if (y < 1 || y > m) return 0;
	return 1;
}

void add(ll x, ll y, ll z) {
	e[++KK] = (node){z, y, le[x], KK + 1}; le[x] = KK;
	e[++KK] = (node){0, x, le[y], KK - 1}; le[y] = KK;
}

bool bfs() {
	for (ll i = 1; i <= tot_num; i++) {
		dis[i] = -1;
		lee[i] = le[i];
	}
	while (!q.empty()) q.pop();
	
	q.push(S);
	dis[S] = 0;
	while (!q.empty()) {
		ll now = q.front();
		q.pop();
		
		for (ll i = le[now]; i; i = e[i].nxt)
			if (e[i].x > 0 && dis[e[i].to] == -1) {
				dis[e[i].to] = dis[now] + 1;
				if (e[i].to == T) return 1;
				q.push(e[i].to);
			}
	}
	
	return 0;
}

ll dfs(ll now, ll sum) {
	if (now == T) return sum;
	
	ll go = 0;
	for (ll &i = lee[now]; i; i = e[i].nxt)
		if (e[i].x > 0 && dis[e[i].to] == dis[now] + 1) {
			ll 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) dis[now] = -1;
	return go;
}

ll dinic() {
	ll re = 0;
	while (bfs())
		re += dfs(S, INF);
	return re;
}

bool check(ll now) {
	csh_wll();
	
	ll needsum = 0;
	S = n * m + 1;
	T = n * m + 2;
	tot_num = T; 
	for (ll i = 1; i <= n; i++)
		for (ll j = 1; j <= m; j++) {
			if ((i + j) & 1) {
				add(S, (i - 1) * m + j, now - a[i][j]);//起点->黑点
				for (ll k = 0; k < 4; k++)
					if (ck(i + dx[k], j + dy[k]))
						add((i - 1) * m + j, (i + dx[k] - 1) * m + j + dy[k], INF);//黑点->它旁边的白点
			}
			else add((i - 1) * m + j, T, now - a[i][j]), needsum += now - a[i][j];//白点->终点
		}
	
	return dinic() == needsum;//判断是否流满,流满就代表可以
}

int main() {
	scanf("%lld", &TI);
	while (TI--) {
		csh();
		
		scanf("%lld %lld", &n, &m);
		
		for (ll i = 1; i <= n; i++)
			for (ll j = 1; j <= m; j++) {
				scanf("%lld", &a[i][j]);
				l = max(l, a[i][j]);
				sum[(i + j) & 1] += a[i][j];
				num[(i + j) & 1]++;
			}
		
		if (num[0] == num[1]) {
			if (sum[0] != sum[1]) {//根据公式可以看出无解
				printf("-1\n");
				continue;
			}
			
			//否则有很多解,要通过二分找到次数最少的,也就是最后加到的值最小的
			ll ans = -1, mid;
			while (l <= r) {
				mid = (l + r) >> 1;
				if (check(mid)) {
					ans = mid;
					r = mid - 1;
				}
				else l = mid + 1;
			}
			
			if (ans == -1) {
				printf("-1\n");
				continue;
			}
			else printf("%lld\n", (n * m * ans - sum[0] - sum[1]) / 2);
		}
		else {
			ll X = (sum[0] - sum[1]) / (num[0] - num[1]);//直接算出答案
			if (X >= l && check(X)) {//要验证
				printf("%lld\n", (n * m * X - sum[0] - sum[1]) / 2);
			}
			else printf("-1\n");
		}
	}
	
	return 0;
}

posted @ 2021-05-19 10:10  あおいSakura  阅读(28)  评论(0编辑  收藏  举报