「SOL」矩阵游戏 (2021省选A卷)
考前感觉啥都复习了
考后感觉啥啥都不会
# 题面
> Link 洛谷 P7515
# 解析
最难处理的是 \(a_{ij}\) 有 \([0,10^6]\) 的限制(因为有这个限制所以我 \(n,m\le3\) 都不会做……)。
假如没有这个限制,显然我们可以随便构造出一组解 \(\{a'_{ij}\}\),下面给出一种构造方法:
- 固定最后一行和最后一列都是 \(0\);
- 从下到上从左到右依次确定 \(a'_{ij}\)(不需要满足范围限制,注意会爆
int
)
然后考虑如何把 \(\{a'_{ij}\}\) 修修补补使它满足范围限制。
这里的构造非常巧妙,我们可以将第 \(i\) 行的偶数列加上 \(p_i\),奇数列减去 \(p_i\),这样操作后每一个 \(2\times2\) 的矩形的和都不变;将第 \(j\) 列的偶数行加上 \(q_j\),奇数行减去 \(q_j\) 同理。
于是我们要求:
差不多转化成了下面这种不等式
这个问题似乎也并不好求解,但是我们会用差分约束求解
怎么消除掉“两个变量之和”的不等式?考虑改变我们的构造方式,原本我们构造的方式是
我们发现,在 \(i,j\) 奇偶性相同的位置,会出现变量之和的限制,于是我们稍作调整:
不难发现,这样构造仍然可以让 \(2\times2\) 的矩形和不变。而这样构造的好处就在于,对应位置的正负性总相反。
然后就可以非常愉快地差分约束。
如果有负环就无解。有负权边,跑 SPFA?反正我会被卡常,看题解发现可以直接 bellman-ford,而且因为是完全二分图,可以用邻接矩阵存边。
跑得特别慢……
最后还有一点小问题。为什么这样是对的呢?我们感觉这个算法看起来就非常对…… 给出一个不是很严谨的证明。
证明
起初对这道题有一个非常暴力的想法:如果确定了 \(\{a_{ij}\}\) 的最后一行和最后一列,那么可以唯一确定矩阵 \(\{a_{ij}\}\)(在忽略范围限制的前提下)。也就是说,我们可以用最后一行、最后一列代表一个矩阵。
假设存在合法解 \(\{a_{ij}\}\),那么通过 \(a_{im}\pm p_i\) 和 \(a_{nj}\pm q_j\),可以得到任意一种 \(a'_{im},a'_{nj}\),也即可以得到任意一种不符合范围限制的矩阵 \(\{a'_{ij}\}\)。
为什么会存在多解呢?因为实际上最后一行和最后一列只能提供 \(n+m-1\) 个方程,而我们有 \(n+m\) 个变量。
# 源代码
/*Lucky_Glass*/
#include <queue>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long llong;
const int N = 305, M = N * N, B = 1e6;
const llong INF = 0x3f3f3f3f3f3f3f3fll;
#define con(typ) const typ &
inline int rin(int &r) {
int b = 1, c = getchar(); r = 0;
while ( c < '0' || '9' < c ) b = c == '-' ? -1 : b, c = getchar();
while ( '0' <= c && c <= '9' ) r = (r * 10) + (c ^ '0'), c = getchar();
return r *= b;
}
void write(con(int) w) {
if ( w < 0 ) putchar('-'), write(-w);
else if ( w < 10 ) putchar('0' + w);
else write(w / 10), putchar('0' + w % 10);
}
int n, m, nedg;
int mtb[N][N];
llong mta[N][N], edga[N][N], edgb[N][N];
llong dis[N << 1];
bool relax(con(int) u, con(llong) len, con(int) v) {
if ( dis[v] > dis[u] + len ) {
dis[v] = dis[u] + len;
return true;
}
return false;
}
bool bellmanFord() {
int cnt = 0; bool tag = false;
for (int i = 1; i <= n + m; i++)
dis[i] = 0;
do {
cnt++, tag = false;
if ( cnt >= n + m ) return false;
for (int u = 1; u <= n + m; u++)
if ( u <= n )
for (int v = 1; v <= m; v++)
tag |= relax(u, edga[u][v], v + n);
else
for (int v = 1; v <= n; v++)
tag |= relax(u, edgb[u - n][v], v);
} while ( tag );
return true;
}
void clean() {
nedg = 0;
fill(dis, dis + n + m + 1, INF);
}
int main() {
int ncas; rin(ncas);
while ( ncas-- ) {
rin(n), rin(m);
clean();
for (int i = 1; i < n; i++)
for (int j = 1; j < m; j++)
rin(mtb[i][j]);
for (int i = n; i; i--)
for (int j = m; j; j--)
if ( i == n || j == m ) mta[i][j] = 0;
else mta[i][j] = mtb[i][j] - mta[i + 1][j + 1]
- mta[i + 1][j] - mta[i][j + 1];
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
if ( (i & 1) ^ (j & 1) )
edga[i][j] = mta[i][j], edgb[j][i] = B - mta[i][j];
else
edgb[j][i] = mta[i][j], edga[i][j] = B - mta[i][j];
if ( !bellmanFord() ) {printf("NO\n"); continue;}
printf("YES\n");
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) {
llong now = mta[i][j];
if ( (i & 1) ^ (j & 1) ) now += dis[i] - dis[j + n];
else now += dis[j + n] - dis[i];
write((int)now), putchar(j == m ? '\n' : ' ');
}
}
return 0;
}
THE END
Thanks for reading!
你走吧 趁着天色未暗
你走吧 深知道阻且难
我献上明月一盏 照满河山
——《山遥路远(人声本家)》By ChiliChill
PS. 省选之后再听这首歌又有不一样的感动,似乎是我将此歌唱给谁人,又像是谁人唱予我。