D. Matrix Cascade

发布时间 2023-08-30 10:55:04作者: onlyblues

D. Matrix Cascade

There is a matrix of size $n \times n$ which consists of 0s and 1s. The rows are numbered from $1$ to $n$ from top to bottom, the columns are numbered from $1$ to $n$ from left to right. The cell at the intersection of the $x$-th row and the $y$-th column is denoted as $(x, y)$.

AquaMoon wants to turn all elements of the matrix to 0s. In one step she can perform the following operation:

  • Select an arbitrary cell, let it be $(i, j)$, then invert the element in $(i, j)$ and also invert all elements in cells $(x, y)$ for $x > i$ and $x - i \ge \left|y - j\right|$. To invert a value means to change it to the opposite: 0 changes to 1, 1 changes to 0.

Help AquaMoon determine the minimum number of steps she need to perform to turn all elements of the matrix to 0s. We can show that an answer always exists.

Input
Each test contains multiple test cases. The first line contains the number of test cases $t$ ($1 \le t \le 10^5$). The description of the test cases follows.

The first line of each test case contains an integer $n$ ($2 \le n \le 3000$).

The $i$-th of the following $n$ lines contains a binary string only of characters 0 and 1, of length $n$.

It is guaranteed that the sum of $n^2$ over all test cases does not exceed $9\,000\,000$.

Output

For each test case, print the minimum number of steps.

Example

input

3
5
00100
01110
11111
11111
11111
3
100
110
110
6
010101
111101
011110
000000
111010
001110

output

1
2
15

Note

In the first test case, we can use the following scheme:

  1. perform the operation on the cell $(1, 3)$.

Clearly, the elements of the initial matrix are not all 0, so at least one operation is required. Thus, $1$ is the answer.

In the second test case, we use the following scheme:

  1. perform the operation on the cell $(3, 3)$;
  2. perform the operation on the cell $(1, 1)$.

It can be shown that there is no way to convert all elements to 0s in $0$ or $1$ steps, so the answer is exactly $2$.

 

解题思路

  先说一下我一开始的做法。

  首先对于矩阵的第$1$行,如果某个格子是$1$那么我们必然至少要对这个格子进行$1$次操作,否则由于操作只会影响往后行的格子那么这个格子不会得到改变。处理完第$1$行后再考虑第$2$行,此时第$1$行已经全为$0$了,如果要对第$1$行操作那么必然是偶数次操作,而对同一个格子进行偶数次操作相当于没有操作,因此我们不能再对之前的行进行操作。对于第$2$行,如果这个格子本来是$1$并且之前行对这个格子影响了偶数次,或者这个位置本来是$0$并且之前行对这个格子影响了奇数次,那么我们就要对这个格子进行一次操作。同理之后的行也是也是这样处理。

  因此我们从上到下遍历每一个格子,如果某个格子本来是$1$并且之前行对这个格子影响了偶数次,或者这个位置本来是$0$并且之前行对这个格子影响了奇数次,那么就对这个格子进行一次操作,最终答案就是进行操作的次数。

  现在的问题就是,如果对某个格子$(i,j)$操作后,如何快速对满足$\left\{(x,y) \mid x > i, \ x - i \ge \left|y - j\right| \right\}$的格子累加一次翻转的次数,可以发现这个格子都是夹在斜率为$1$和$-1$的直线之间的格子,如下图,其中斜率为$1$和$-1$的直线用红色标出来:

  假设原点$(0,0)$在左上角,那么每个格子都可以对应一条斜率为$1$和$-1$的直线。斜率为$1$的直线可以表示为$y=x+b$,那么我们可以用截距$b=y-x$来表示格子$(i,j)$属于斜率为$1$截距为$j-i$的直线。同理用$j+i$来表示格子$(i,j)$属于斜率为$-1$截距为$j+i$的直线。为了方便,这里对斜率为$1$的直线的截距都加上偏移量$n$,使得所有截距都映射到$[1,2n-1]$,对斜率为$1$的直线的截距都加上偏移量$1$,使得所有截距也映射到$[1,2n-1]$。

  对格子$(i,j)$操作后,我们先对斜率为$1$,截距在$[1, j-i+n]$所对应直线的所有格子都加上$1$,如下图:

  再对斜率为$-1$,截距在$[1, j+i+1-1]$所对应直线的所有格子都加上$-1$,如下图:

  这样就可以对$(i,j)$所能影响到的格子都累加一次翻转的次数。可以发现这样做的话在上面的某些格子会被$-1$,不过这个没关系,因为上面行的格子我们不会再进行操作了,只要保证同行以及下面行的格子没受到影响就行。

  由于涉及到区间动态累加的操作,因此我们可以用树状数组来维护一个差分数组,这样就可以实现区间修改,单点查询。另外由于我们只关心某个格子被影响了奇数次还是偶数次,并且每次操作都是$+1$和$-1$,因此可以把所有的运算转换成异或,相当于模$2$加法。

  AC代码如下,时间复杂度为$O(n^2 \log{n})$:

 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 
 4 const int N = 3010;
 5 
 6 int n;
 7 char g[N][N];
 8 int tr1[N * 2], tr2[N * 2];
 9 
10 int lowbit(int x) {
11     return x & -x;
12 }
13 
14 void add(int *tr, int x) {
15     for (int i = x; i <= n << 1; i += lowbit(i)) {
16         tr[i] ^= 1;
17     }
18 }
19 
20 int query(int *tr, int x) {
21     int ret = 0;
22     for (int i = x; i; i -= lowbit(i)) {
23         ret ^= tr[i];
24     }
25     return ret;
26 }
27 
28 void solve() {
29     scanf("%d", &n);
30     for (int i = 0; i < n; i++) {
31         scanf("%s", g[i]);
32     }
33     memset(tr1, 0, n * 2 + 10 << 2);
34     memset(tr2, 0, n * 2 + 10 << 2);
35     int ret = 0;
36     for (int i = 0; i < n; i++) {
37         for (int j = 0; j < n; j++) {
38             int k1 = j - i + n, k2 = j + i + 1;
39             if (g[i][j] + query(tr1, k1) + query(tr2, k2) & 1) {
40                 ret++;
41                 add(tr1, 1), add(tr1, k1 + 1);
42                 add(tr2, 1), add(tr2, k2);
43             }
44         }
45     }
46     printf("%d\n", ret);
47 }
48 
49 int main() {
50     int t;
51     scanf("%d", &t);
52     while (t--) {
53         solve();
54     }
55     
56     return 0;
57 }

  在参考别人的做法后发现可以优化到$O(n^2)$。

  我们反过来考虑每个格子$(i,j)$会被哪些格子影响到,很明显就是满足$\left\{(x,y) \mid x < i, \ i - x \ge \left|y - j\right| \right\}$的格子,如下图:

  然后我们维护三个数组,$a_{i,j}$表示在斜率为$1$且经过$(i,j)$的直线上(即上图蓝色的部分),从直线最左上角的格子到$(i,j)$翻转的次数的累加,因此有$a_{i,j} = a_{i-1,j-1}$。$b_{i,j}$表示在斜率为$-1$且经过$(i,j)$的直线上(即上图绿色的部分),从直线最右上角的格子到$(i,j)$翻转的次数的累加,因此有$b_{i,j} = b_{i-1,j+1}$。$c_{i,j}$表示所有能影响到$(i,j)$格子的翻转的次数的累加,因此有$c_{i,j} = c_{i-1,j} + a_{i-1,j-1} + b_{i-1,j+1}$。这样就知道了格子$(i,j)$被影响到的次数$c_{i,j}$了。

  AC代码如下,时间复杂度为$O(n^2)$:

 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 
 4 const int N = 3010;
 5 
 6 char g[N][N];
 7 int a[N][N], b[N][N], c[N][N];
 8 
 9 void solve() {
10     int n;
11     scanf("%d", &n);
12     for (int i = 1; i <= n; i++) {
13         scanf("%s", g[i] + 1);
14     }
15     int ret = 0;
16     for (int i = 1; i <= n; i++) {
17         for (int j = 1; j <= n; j++) {
18             a[i][j] = a[i - 1][j - 1];
19             b[i][j] = b[i - 1][j + 1];
20             c[i][j] = c[i - 1][j] ^ a[i - 1][j - 1] ^ b[i - 1][j + 1];
21             if (c[i][j] ^ g[i][j] - '0') {
22                 ret++;
23                 a[i][j] ^= 1;
24                 b[i][j] ^= 1;
25                 c[i][j] ^= 1;
26             }
27         }
28     }
29     printf("%d\n", ret);
30 }
31 
32 int main() {
33     int t;
34     scanf("%d", &t);
35     while (t--) {
36         solve();
37     }
38     
39     return 0;
40 }

 

参考资料

  Harbour.Space Scholarship Contest 2023-2024 (Div. 1 + Div. 2) Editorial:https://codeforces.com/blog/entry/119772

  Harbour.Space Scholarship Contest 2023-2024 (Div. 1 + Div. 2):https://www.cnblogs.com/cjjsb/p/17662353.html