题解【P7515 [省选联考 2021 A 卷] 矩阵游戏】
\(\text{Update 2022.9.29}\):经 @foreverchan 提醒,增加了有关做法正确性的证明,并修改了一些语句。
一年前我在省选赛场上折戟沉沙,一年后我从倒下的地方再摔一跤。
我成功了,我仍然是以前的那个我。
神题,希望自己讲得清楚一些。
先不考虑 \(0\le a_{i,j}\le 10^6\) 的限制,只考虑 \(b_{i,j}=a_{i,j}+a_{i+1,j}+a_{i,j+1}+a_{i+1,j+1}\),瞎鸡巴填一手,得到矩阵 \(A\),之后比较自然的想法就是通过 \(+1\) 和 \(-1\) 来微调这个东西。
其实这个时候差不多就可以想到差分约束了,因为我们最终希望把每一个 \(a_{i,j}\) 变成 \(0\le a_{i,j}\le 10^6\) 这样一个范围的形式。不过我知道你很急,但你先别急。继续往下做,看看这个想法有没有希望。
两个比较显然的性质:
-
对于每一行轮流进行 \(+1\) 和 \(-1\) 的操作,结果不变。
-
对于每一列轮流进行 \(+1\) 和 \(-1\) 的操作,结果不变。
因为任意一个 \(2\times 2\) 的矩阵中的一组 \(+1\) 和 \(-1\) 会相互抵消掉。
我们设 \(1\) 操作在第 \(i\) 行进行了 \(c_{i}\) 次,设 \(2\) 操作在第 \(j\) 行进行了 \(d_i\) 次,那么可以得到如下的矩阵(每一项省略了 \(a_{i,j}\)):
第一眼:哇这不是差分约束板子吗!
第二眼:我是不是应该发明一个叫 \(0\le a_{i,j}+c+d\le 10^6\) 的和分约束?
但是这个和分约束显然可以通过变化 \(c\) 的符号来转化成差分约束。
我们将偶数行的 \(c\) 变成相反数,奇数列的 \(d\) 变成相反数。
那么原矩阵就变成了:
这里仅举例 \(2\times 2\) 的矩阵已经足够证明正确性,因为包含了所有下标的奇偶性,这样的话转化就做完了。
我们先将第最后一行钦定为 \(0\),然后得到了一个不考虑限制的矩阵 \(A\),之后按照上面的矩阵,以 \((1,1)\) 为例,得到差分约束 \(0\le a_{1,1}+c_1-d_1\le 10^6\) 的形式,跑 \(\text{SPFA}\) 就行了。
注意,这道题可以不用建立虚点的,因为这张图已经保证联通了,而且是差不多是一张完全图,用链式前向星容易被卡,而 \(\texttt{vector}\) 存图在稠密图的情况下是表现良好的。
证明一下这个做法的正确性,钦定 \(a_{i,j}\) 会有 \((n-1)(m-1)\) 个方程,差分约束会建立 \(2nm\) 个方程,但这里的方程每有两个会得到 \((n+m)\) 个未知数,每个未知数会得到一个 \(a_{i,j}\),所以本质是有 \((n+m)\) 个方程,最终我们得到 \((nm+1)\) 个有关 \(a\) 的方程,足以确定 \(nm\) 个未知数。
个人感觉比较好看的代码:
const int MAXN(310);
const int MAXM(610);
const int MAX(1e6);
int n,m,a[MAXN][MAXN],b[MAXN][MAXN],K;
struct node{int to,cost;};
vector<node>G[MAXM];
bool inq[MAXM];
queue<int>q;
ll d[MAXM];
int cnt[MAXM];
inline void add_edge(int u,int v,int w)
{
node res;
res.to=v,res.cost=w;
G[u].push_back(res);
return;
}
inline bool spfa(int s)
{
rep(i,1,n+m) d[i]=LLINF,inq[i]=false,cnt[i]=0;
d[s]=0;
q.push(s);
while(!q.empty())
{
int u=q.front();
q.pop();
inq[u]=false;
rep(i,0,(int)G[u].size()-1)
{
node e=G[u][i];
if(d[e.to]>d[u]+e.cost)
{
d[e.to]=d[u]+e.cost;
if(!inq[e.to])
{
if(++cnt[e.to]>=n+m) return false;
inq[e.to]=true,q.push(e.to);
}
}
}
}
return true;
}
inline void solve()
{
n=Fread::read(),m=Fread::read();
rep(i,1,n-1) rep(j,1,m-1) b[i][j]=Fread::read();
rev(i,n-1,1) rev(j,m-1,1) a[i][j]=b[i][j]-a[i+1][j]-a[i][j+1]-a[i+1][j+1];//随便构造一个矩阵 A
rep(i,1,n) rep(j,1,m)
{
if((i+j)&1) add_edge(j+n,i,MAX-a[i][j]),add_edge(i,j+n,a[i][j]);
else add_edge(i,j+n,MAX-a[i][j]),add_edge(j+n,i,a[i][j]);
}
if(!spfa(1)) puts("NO");
else
{
puts("YES");
rep(i,1,n)
{
rep(j,1,m)
{
int now=d[i]-d[j+n];
if((i+j)&1) cout<<a[i][j]+now,putchar('\n');
else cout<<a[i][j]-now,putchar('\n');
}
putchar('\n');
}
}
Clear(a);
rep(i,1,n+m) G[i].clear();
return;
}
int main()
{
int T=Fread::read();
while(T--) solve();
return 0;
}
/*
Date : 2022/9/27
Author : UperFicial
Start coding at : 17:03
finish debugging at : 17:50
*/