CSDN竞赛65期题解

总结

由于考试报告是空白,只分享下 T 2 T2 T2的解题思路吧。虽然叫C站,这次题目直接以python列表的形式输入和输出,输入C选手勉强能忍,进行下字符串分隔进行,至于输出,就让人完全没有使用C作答的想法了,而且一级列表不够还给了二级列表,可见,选题的负责人大概率是不懂编程的。按照标准的数组输入输出,不管是C还是python都可以很快处理,偏偏选择了这种python风格的输入输出。

这次 T 2 T2 T2题目还行,出自Leetcode,看不见考试报告,就直接用leetcode的原题来分析下吧。

leetcode1632. 矩阵转换后的秩

题目描述

给你一个 m x n 的矩阵 matrix ,请你返回一个新的矩阵 answer ,其中 answer[row][col]matrix[row][col] 的秩。

每个元素的 是一个整数,表示这个元素相对于其他元素的大小关系,它按照如下规则计算:

  • 秩是从 1 开始的一个整数。
  • 如果两个元素 p和 q 在同一行或者同一列,那么:
    • 如果 p < q ,那么 rank(p) < rank(q)
    • 如果 p == q ,那么 rank(p) == rank(q)
    • 如果 p > q ,那么 rank(p) > rank(q)
  • 需要越 越好。

题目保证按照上面规则 answer 数组是唯一的。

分析

题目出自leetcode 1632. 矩阵转换后的秩。

题目意思很清晰,首先不考虑重复元素的情况。如果是一维数组,那么我们只需要对数组进行排序,每个元素的秩 k k k表示这个元素在数组中是第 k k k小的。排序过程中我们无意中维护了一组偏序关系,即有序的数组里前面元素都不大于后面的元素。对于矩阵而言,我们是否能够对元素所在行和所在列的元素分别进行排序后,将每个元素的秩设置为其行秩和列秩中的较大者呢?

比如

4 1

3 2

以上面的矩阵为例,元素4在行序列中的秩为2,在列序列中的秩也是2,如果我们设置 r a n k ( 4 ) = 2 rank(4)=2 rank(4)=2,结果就不对了。矩阵中最小的元素是1,秩为1,2和1在同一列,秩为2,3和2在同一行,秩为3,4和3在同一列,秩为4,显然不是前面设置的2。之所以不能简单的取行秩和列秩的较大者,是因为偏序关系具有传递性。

为了维护矩阵整体的偏序关系,我们可以建个图来维护,比如第一行可以建立1到4的有向边,那么最后就可以将矩阵转换为一个有向图。

1
4
2
3

1节点的入度为0,表示同一行同一列没有比它小的元素,我们就可以使用拓扑排序来求出每个元素的秩,首先 r a n k ( 1 ) = 1 rank(1)=1 rank(1)=1,去掉1指向的边,之后2号节点的入度变成了0,其秩为1的秩加上1,即 r a n k ( 2 ) = 2 rank(2)=2 rank(2)=2,再消去2指向的边,3入度变成0,得到 r a n k ( 3 ) = 3 rank(3)=3 rank(3)=3,最后得到 r a n k ( 4 ) = 4 rank(4)=4 rank(4)=4

显然在同一行(列)没有相等元素的情况下,我们只需要将矩阵转换为有向图,再做次拓扑排序就可以解决本题了,但是加上存在相等元素且同一行(列)相等元素的秩相同的条件后,问题就稍微复杂了。由于相等元素的秩相等,所以同行(列)相等元素在矩阵转换为图的时候可以视为一个节点,这就要用到图论里面常见的缩点技巧了,将同一行、同一列的相等元素缩小为一个节点,再建立图,就可以得到一个DAG,做下拓扑排序就可以求解了。

举个例子,第一行第一列元素可能与第一行第三列元素相等,第一行第三列元素又与第三行第三列相等,这些点都需要缩为一个节点,所以可以选择其中的一个点作为标志,这就很符合并查集的语义了,选择一个节点作为集合的根节点,这个节点代表着这个集合。

所以本题的求解思路就是:

  • 逐行和逐列遍历矩阵,将相等的元素加入到同一个集合中。
  • 再次逐行和逐列对元素进行排序,将元素之间的偏序关系维持为一个有向图。
  • 进行拓扑排序,求出每个元素的秩。

这题的难度不仅在于求解思路,还在于编码难度,比较考验编码功底。下面说下几个编码的技巧:

节点的编号:由于只有同一行(列)相等的元素才能缩为一个节点,所以存在缩点后DAG中不同节点的值相等的情况,因此不能使用元素的值作为节点的编号,直接使用矩阵的行号和列号作为节点的编号也不合适,可以将二维矩阵映射为一维数组,将映射后的下标作为有向图节点的编号。

如何将同一行(列)元素加入相同的集合,并且构造偏序关系的有向图呢?由于我们使用下标作为编号,所以需要对值和下标做个逆映射,同一行可能存在相等的元素,一些人会想到将值映射为一个vector,这就加大了编码的难度了。其实我们在遍历一行元素时,只需要记录每个元素第一次出现的位置,当遍历到某个元素之前出现过,就可以将这个元素的编号加入到之前出现过元素的集合里了。

做好逆映射和维护好并查集后,就可以开始建立有向图了,遍历到一行(列),就可以对其进行排序,遍历下有序序列,如果某个元素不等于上一个元素,就将建立一条上个元素到该元素的有向边。最后进行拓扑排序,在拓扑排序过程中顺带求解下矩阵的秩。

最后要注意下:尽管建立并查集和建立有向图的操作都需要逐行逐列遍历元素,但是我们不能将两个操作合并到一个迭代里,比如给一行元素建立并查集后立刻建立偏序关系。这是因为我们需要将矩阵中要缩的点都缩完才能进行建图,否则的话开始节点A在一个有向图里,后面遇见节点B在另一个有向图里,发现这两个节点在一个集合里,再进行合并就很困难了。

详细解法可以参考下面代码,思路还是比较清晰的。

代码

class Solution {
public:
    const static int N = 250005;
    int n, m;
    int idx,h[N],e[N],ne[N];
    int fa[N];
    int ind[N];
    queue<int> q;
    vector<int> res;
    unordered_map<int,int> m_row[505];
    unordered_map<int,int> m_col[505];
    int to(int x,int y) {
        return x * m + y;
    }
    int find(int x) {
        if (fa[x] != x) fa[x] = find(fa[x]);
        return fa[x];
    }
    void add(int a,int b) {
        e[idx]=b,ne[idx]=h[a],h[a]=idx++;
    }
    vector<vector<int>> matrixRankTransform(vector<vector<int>>& matrix) {
        n = matrix.size();
        m = matrix[0].size();
        for (int i = 0; i < n * m;i++) {
            fa[i] = i;
        }
        memset(h,-1,sizeof h);
        //逐行缩点
        for(int i = 0;i < n;i++) {
            vector<int> t = matrix[i];
            for (int j = 0;j < m;j++) {
                int v = to(i, j);
                if(m_row[i].count(t[j])) {
                    int a = find(v), b = find(m_row[i][t[j]]);
                    fa[a] = b;
                } else {
                    m_row[i][t[j]] = v;
                }
            }
        }
        //逐列缩点
        for (int i = 0; i < m;i++) {
            for(int j = 0;j < n;j++) {
                int v = to(j, i);
                auto &t = matrix[j][i];
                if (m_col[i].count(t)) {
                    int a = find(v), b = find(m_col[i][t]);
                    fa[a] = b;
                } else {
                    m_col[i][t] = v;
                }
            }
        }
        //逐行添加偏序关系
        for(int i = 0;i < n;i++) {
            vector<int> t = matrix[i];
            sort(t.begin(), t.end());
            for (int j = 1;j < m;j++) {
                if (t[j] == t[j - 1]) continue;
                int a = find(m_row[i][t[j-1]]), b = find(m_row[i][t[j]]);
                add(a, b);
                ind[b]++;
            }
        }
        //逐列添加偏序关系
        for (int i = 0; i < m;i++) {
            vector<int> t;
            for (int j = 0;j < n;j++) {
                t.push_back(matrix[j][i]);
            }
            sort(t.begin(), t.end());
            for(int j = 1;j < n;j++) {
                if (t[j] == t[j - 1]) continue;
                int a = find(m_col[i][t[j-1]]), b = find(m_col[i][t[j]]);
                add(a, b);
                ind[b]++;
            }
        }
        res.resize(N);
        for (int i = 0;i < n * m; i++) {
            fa[i] = find(i);
            if(fa[i] == i && !ind[i]) {
                q.push(i);
                res[i] = 1;
            }
        }
        while(q.size()) {
            int u = q.front();
            q.pop();
            for (int i = h[u];~i;i=ne[i]) {
                int j = e[i];
                ind[j]--;
                if (!ind[j]) {
                    res[j] = res[u] + 1;
                    q.push(j);
                }
            }
        }
        vector<vector<int> > rank;
        for (int i = 0;i < n;i++) {
            vector<int> t;
            for(int j = 0;j < m;j++) {
                int v = to(i, j);
                t.push_back(res[fa[v]]);
            }
            rank.push_back(t);
        }
        return rank;
    }
};

你可能感兴趣的:(其它,并查集,拓扑排序,缩点)