Loading

Matrix Transformation

思路

首先观察到二进制数可以被拆分

所以问题转化成一个 \(01\) 矩阵, 其中你可以对每一行清零, 对每一列设为全 \(1\)

手动模拟样例可以发现, 无解仅当会出现类似于环形结构

我们考虑建图

你发现对于一个确定的点 \((i, j)\) ,

  • 如果其值为 \(0\) , 那么 \(i\) 行的清零操作一定在 \(j\) 列的全 \(1\) 操作之后
  • 如果其值为 \(1\) , 那么 \(j\) 行的全 \(1\) 操作一定在 \(i\) 行的清零操作之后

我们考虑对于这些点来建图, 如果是 \(\rm{DAG}\) , 那么有解, 否则无解
因为你从一个操作绕一圈依赖回了自己肯定是不行的
需要注意的是连边 \(u \to v\) 的含义是如果有 \(u\) 且有 \(v\) , 那么必定有 \(v\) 在之前, 而非 \(u, v\) 必须都选择

你注意到如果这样做多半是假的, 因为这样跟初始矩阵甚至没有关系了

考虑观察初始矩阵对其有什么影响, 容易发现的是有些行列我们不需要处理, 怎么在图上表示这个条件

需要注意的是即使这个行列我们没有数直接要求处理, 其约束条件依然存在

最后我们只需要对于所有 一定需要操作的 行 / 列 所在的连通块判环即可


考虑复习
很破防的是似乎 \(\textrm{div 2}\) 要做到 \(\rm{E}\) 才能够到 \(\textrm{NOIP 1=}\) , 哈哈哈
哈哈哈
那真的很需要每日一练了
这周想了一下, 可能包括我的父母都认为搞竞赛是一个错误的决定了吧, 哈哈哈

  • 定义操作, 要求把 \(a \to b\)
    • 逆向思维, 把 \(b\) 拆成 \(a\)
      • \(\rm{STL}\) 维护
    • 考虑操作是否能构造出单位操作, 在用单位操作的性质去做题 \((\)一般是到了哪里就不能操作了\()\)
    • 打印操作方法
      • 考虑一种构造策略
    • 约束仅存在于操作顺序上
      • 考虑对约束顺序建图判环
题意

给定大小为 n×mn \times m 的矩阵 A,BA, B
AA 可以进行以下操作任意次任意顺序

  • 选择某一行 ii 和一个非负整数 xx, 将这一行的每个元素用 xx 进行按位与运算
  • 选择某一列 ii 和一个非负整数 xx, 将这一列的每个元素用 xx 进行按位或运算

求是否能将 ABA \to B

先简化问题, 每个行列只能与/或一个值, 不影响答案
不存在进位, 把 \(A, B\) 拆成 \(01\) 矩阵处理

那很舒服了, 不难发现与/或一个值也限制在 \(0, 1\) 中了

但是每个行列只能与/或一个值好像会影响答案, 急急急

方法一

不难发现 与运算只考虑 \(1\) , 或运算只考虑 \(0\)
所以尝试构造方式, 先把一个行变成 \(1\) , 然后再用列操作一个一个转化成目标矩阵

什么时候不可以这么做?
对于一个行, 我们可以知道其是否需要行操作, 进而处理出需要哪些列操作
但是你发现不能直接从第 \(1 \sim n\) 行这样模拟, 因为可能先处理其他行是合法的, 也就是说可能出现行的排列方式产生的问题

不妨把格局打开, 一个位置对操作顺序的要求是什么

  • \(1 \to 0\)
    对当前行的赋 \(1\) 操作应当在对当前列的赋 \(0\) 操作之前
  • \(0 \to 1\)
    对当前行的赋 \(1\) 操作应当在对当前列的赋 \(0\) 操作之后

因此对每一行列的 \(0, 1\) 操作建边, 出现环则无解

方法二

转化问题变成对于 \(01\) 矩阵, 可以将一行变成 \(0\) 或者将一列变成 \(1\), 判断是否可以得到另一个矩阵

逆向思维, 也就是是否可以通过删除一个矩阵的全 \(0\) 行和全 \(1\) 列使其变为目标矩阵
乱跑

代码

放一份 \(\rm{QCZ}\) 大佬的代码, 好看爱看

#include <bits/stdc++.h>
using namespace std;

int n, m, a[1010][1010], b[1010][1010], vis[4010], instk[1010], flag;
int head[4010], nxt[4010], to[4010], cnte;
void addedge (int u, int v) { to[++cnte] = v, nxt[cnte] = head[u], head[u] = cnte; }

void dfs (int u) {
	if (instk[u]) { flag = 1; return ; }
	if (vis[u]) return ;
	instk[u] = vis[u] = 1;
	for (int i = head[u]; i; i = nxt[i]) dfs (to[i]);
	instk[u] = 0;
}

void solve () {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) scanf("%d", &a[i][j]);
	for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) scanf("%d", &b[i][j]);
	for (int i = 0; i < 30; i++) {
		cnte = flag = 0; for (int i = 0; i <= n + m + 10; i++) head[i] = vis[i] = instk[i] = 0;
		for (int x = 1; x <= n; x++) for (int y = 1; y <= m; y++) if (((b[x][y] >> i) & 1) == 1) addedge (x, n + y);
		for (int y = 1; y <= m; y++) for (int x = 1; x <= n; x++) if (((b[x][y] >> i) & 1) == 0) addedge (n + y, x);
		for (int x = 1; x <= n; x++) for (int y = 1; y <= m; y++) if (((a[x][y] >> i) & 1) != ((b[x][y] >> i) & 1)) {
			addedge (0, ((a[x][y] >> i) & 1) ? x : n + y);
		}
		dfs (0); if (flag) { puts ("No"); return ; }
	}
	puts ("Yes");
}

signed main () {
	int t; scanf("%d", &t);
	while (t--) solve ();
	return 0;
}

总结

二进制拆分可以将问题简化到 \(01\)
一般来说只有操作型问题可以建图, 建图的关键是根据结果倒退怎样操作
超级源点可以解决一类求子图问题

一般来说, 在 \(01\) 问题上, 与运算只考虑 \(1\) , 或运算只考虑 \(0\)
对操作顺序的约束, 往往可以拆分到具体元素上

逆向思维经典

posted @ 2024-12-25 16:03  Yorg  阅读(102)  评论(0)    收藏  举报