构造新矩阵

构造新矩阵

给定一个 $m$ 行 $n$ 列的整数矩阵,行编号 $1 \sim m$,列编号 $1 \sim n$。

其中,第 $i$ 行第 $j$ 列的元素为 $p_{ij}$。

你可以任意抽取其中不超过 $n−1$ 行元素,这些元素之间保持同一行列关系不变,构成一个新矩阵。

构成新矩阵后,我们可以确定一个最大的整数 $L$,使得新矩阵中每一列都至少存在一个元素不小于 $L$。

我们希望通过合理构造新矩阵,使得 $L$ 的值尽可能大。

请你计算并输出 $L$ 的最大可能值。

注意:矩阵一共有 $m$ 行,但是抽取的行数上限是 $n−1$ 行,而不是 $m−1$ 行,读题时不要搞混行和列。

输入格式

第一行包含整数 $T$,表示共有 $T$ 组测试数据。

每组数据首先包含一个空行。

第二行包含两个整数 $m,n$。

接下来 $m$ 行,每行包含 $n$ 个整数,其中第 $i$ 行第 $j$ 个整数表示 $p_{ij}。

输出格式

每组数据输出一行结果,一个整数,表示 $L$ 的最大可能值。

数据范围

前三个测试点满足 $1 \leq T \leq 5$,$2 \leq n \times m \leq 100$。
所有测试点满足 $1 \leq T \leq {10}^4$,$2 \leq n$,$2 \leq n \times m \leq {10}^5$,$1 \leq p_{ij} \leq {10}^9$,一个测试点内所有数据的 $n \times m$ 值相加不超过 ${10}^5$。

输入样例:

5

2 2
1 2
3 4

4 3
1 3 1
3 1 1
1 2 2
1 1 3

2 3
5 3 4
2 5 1

4 2
7 9
8 1
9 6
10 8

2 4
6 5 2 1
7 9 7 2

输出样例:

3
2
4
8
2

 

解题思路

  题目的意思是构造的矩阵的每一列的最大元素都要不小于$L$。首先答案具有二段性,假设$L$的最大取值为$\text{ans}$,那么很明显$L$是不可能大于$\text{ans}$,否则就与假设矛盾,而如果$L = \text{ans}$存在一种方案,那么当$L$的取值小于$\text{ans}$该方案也是成立的,因此可以二分答案。

  难的地方在于$\text{check}$函数,先给出我比赛时的写法。对于二分得到的$\text{mid}$,先把矩阵中大于等于$\text{mid}$的值用$\text{bool}$矩阵标记出来,同时统计每一行标记了多少个元素。然后按照元素数量从大到小排序,从标记元素最多的行开始遍历,同时开个$\text{vis}$数组用来记录构造矩阵的每一列是否存在大于等于$L$的元素。如果发现某一列还没有被标记(指之前选取的行在该列都没有大于等于$L$的元素),那么我们就选这一行,同时把这行所有能标记的列都标记到$\text{vis}$数组中对应的列。

  很明显这是一种贪心做法,如果发现某一列不存在大于等于$L$的元素,那么很明显我们应该从该列被标记的行这个集合中选,而在这个集合中我们又应该选择标记元素最多的那一行,因为这一行标记的列最多,选择这一行肯定比选择其他标记少的行的效果要好。

  最后判断所选的行数是否不超过$m-1$(这里的$m$指列),且$\text{vis}$数组是否被全部标记。

  AC代码如下,时间复杂度为$O\left( {\log{10^9} \times \left( {nm + n\log{n}} \right)} \right)$:

 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 
 4 const int N = 1e5 + 10;
 5 
 6 int n, m;
 7 vector<int> g[N];
 8 vector<bool> st[N];
 9 int cnt[N], p[N];
10 bool vis[N];
11 
12 bool check(int mid) {
13     for (int i = 0; i < n; i++) {
14         st[i] = vector<bool>(m);
15         cnt[i] = 0, p[i] = i;
16         for (int j = 0; j < m; j++) {
17             if (g[i][j] >= mid) {
18                 cnt[i]++;
19                 st[i][j] = true;
20             }
21         }
22     }
23     sort(p, p + n, [&](int i, int j) {
24         return cnt[i] > cnt[j];
25     });
26     memset(vis, 0, m);
27     int s = 0;
28     for (int i = 0; i < n; i++) {
29         for (int j = 0; j < m; j++) {
30             if (!vis[j] && st[p[i]][j]) {
31                 if (++s >= m) return false;
32                 for (int k = 0; k < m; k++) {
33                     vis[k] |= st[p[i]][k];
34                 }
35                 break;
36             }
37         }
38     }
39     for (int i = 0; i < m; i++) {
40         if (!vis[i]) return false;
41     }
42     return true;
43 }
44 
45 void solve() {
46     scanf("%d %d", &n, &m);
47     for (int i = 0; i < n; i++) {
48         g[i].resize(m);
49         for (int j = 0; j < m; j++) {
50             scanf("%d", &g[i][j]);
51         }
52     }
53     int l = 1, r = 1e9;
54     while (l < r) {
55         int mid = l + r + 1 >> 1;
56         if (check(mid)) l = mid;
57         else r = mid - 1;
58     }
59     printf("%d\n", l);
60 }
61 
62 int main() {
63     int t;
64     scanf("%d", &t);
65     while (t--) {
66         solve();
67     }
68     
69     return 0;
70 }

  再给出y总的做法。

  首先如果$n \leq m - 1$,那么很明显选择所有的行,$L$的取值就是矩阵中每一列最大值的最小值。

  然后是$n > m - 1$的情况。在$\text{check}$函数中,如果发现某一列$j$没有标记元素,即每一行的第$j$列的值都小于$\text{mid}$,那么无解。然后再考虑每一列都有标记元素的情况,假设题目要求选取$m$行那么肯定是有解的,因为每一列都有标记元素,对应着至多选$m$行就可以覆盖每一列。而如果选$m-1$行呢?意味着至少存在一行能够覆$2$列,此时这一行覆盖了$2$列,那么剩下的$m-2$行至少能覆盖$m-2$列,是一种可行的构造方案。

  因此在$\text{check}$函数中,遍历每一列,先看看这一列是否存在被标记元素(没有则回传$\text{false}$),然后如果枚举到某行发现之前这行之前标记过元素,那么意味着至少存在一行能覆盖两列。对于$n \leq m - 1$的情况也可以按照这种方式来判断,因为如果每一列都存在标记元素,那么根据抽屉原理必然至少存在一行标记了两个或以上的元素。

  AC代码如下,时间复杂度为$O\left( {\log{10^9} \times nm} \right)$:

 1 #include <bits/stdc++.h>
 2 using namespace std;
 3 
 4 const int N = 1e5 + 10;
 5 
 6 int n, m;
 7 vector<int> g[N];
 8 bool vis[N];
 9 
10 bool check(int mid) {
11     memset(vis, 0, n);
12     bool same = false;
13     for (int j = 0; j < m; j++) {
14         bool flag = false;
15         for (int i = 0; i < n; i++) {
16             if (g[i][j] >= mid) {
17                 flag = true;
18                 if (vis[i]) same = true;
19                 vis[i] = true;
20             }
21         }
22         if (!flag) return false;
23     }
24     return same;
25 }
26 
27 void solve() {
28     scanf("%d %d", &n, &m);
29     for (int i = 0; i < n; i++) {
30         g[i] = vector<int>(m);
31         for (int j = 0; j < m; j++) {
32             scanf("%d", &g[i][j]);
33         }
34     }
35     int l = 1, r = 1e9;
36     while (l < r) {
37         int mid = l + r + 1 >> 1;
38         if (check(mid)) l = mid;
39         else r = mid - 1;
40     }
41     printf("%d\n", l);
42 }
43 
44 int main() {
45     int t;
46     scanf("%d", &t);
47     while (t--) {
48         solve();
49     }
50     
51     return 0;
52 }

 

参考资料

  AcWin 4863. 构造新矩阵(AcWing杯 - 周赛):https://www.acwing.com/video/4628/

posted @ 2023-02-20 17:14  onlyblues  阅读(69)  评论(0编辑  收藏  举报
Web Analytics