LeetCode 1349. 参加考试的最大学生数 (状压DP 或 二分图最大独立子集)

给你一个 m * n 的矩阵 seats 表示教室中的座位分布。如果座位是坏的(不可用),就用 '#' 表示;否则,用 '.' 表示。

学生可以看到左侧、右侧、左上、右上这四个方向上紧邻他的学生的答卷,但是看不到直接坐在他前面或者后面的学生的答卷。请你计算并返回该考场可以容纳的一起参加考试且无法作弊的最大学生人数。

学生必须坐在状况良好的座位上。

示例 1:

输入:seats = [["#",".","#","#",".","#"],
              [".","#","#","#","#","."],
              ["#",".","#","#",".","#"]]
输出:4
解释:教师可以让 4 个学生坐在可用的座位上,这样他们就无法在考试中作弊。 

提示

seats 只包含字符 '.''#'
m == seats.length
n == seats[i].length
1 <= m <= 8
1 <= n <= 8

1. 状压DP

首先一看数据范围,一个学过状压DP的人就应该想到它……个人经验,数据范围在 10 左右的时候,就可以考虑下是不是状压DP。

用二进制的每一位去枚举一个位置是否坐了人,一行有n个座位,则一共有1<<n种情况。

dp[i][j]用来第i行状态为j的最大值,那么dp[i][j]=j状态包含1的个数+max(dp[i-1][k]) (0<=k<(1<<n) && j 和 k 都是合法状态 && j和k不存在能够相互抄袭的两个位置)

比赛时代码写的比较丑陋 写80多行……赛后参考了一下题解,使用 j&(j<<1) 来判断是否有相邻的点就比较厉害了~

AC代码(cpp)

class Solution {
    int countOne(int x) {
        int cnt = 0;
        while(x != 0) x = x & (x - 1), cnt++;
        return cnt;
    }
public:
    int maxStudents(vector<vector<char>>& seats) {
        int n = seats.size();
        int m = seats[0].size();
        int st = 1 << m;

        vector<vector<int>> dp(n, vector<int>(st, 0));
        for (int i = 0; i < n; i++) {
            int mask = 0;
            for (int j = 0; j < m; j++) {
                mask += seats[i][j] == '#' ? (1 << j) : 0;
            }
            for (int j = 0; j < st; j++) {
                if (!(j & mask) && !(j & (j << 1))) {
                    int v = countOne(j);
                    if (i == 0) dp[i][j] = v;
                    else {
                        for (int k = 0; k < st; k++) {
                            if (!((k << 1) & j) && !((k >> 1) & j)) {
                                dp[i][j] = max(dp[i][j], dp[i - 1][k] + v);
                            }
                        }
                    }
                }
            }
        }
        int ans = 0;
        for (int i = 0; i < st; i++) {
            ans = max(dp[n - 1][i], ans);
        }

        return ans;
    }
};

2. 二分图最大匹配

太久没有写过二分图已经没什么印象了,经群里大神提示,才又复习了一遍之前的博客。

二分图判断(交叉染色)

二分图最大匹配(匈牙利算法)

此题如果把每个能抄袭的两个位置连接,求最大可以容纳的考生数就是求没内部有边的最大集合,即最大独立子集。

同时又知道二分图属性 

最大独立数 =  顶点数 — 最大匹配数

那么此题转化为求二分图最大匹配。

有一点需要注意的是,匈牙利模板中,是将两个点集分开的情况下去求最大匹配的,而类似本题是没有规定哪个点属于哪个点集,

所以可以想象把每个点拆成两个点,每个点属于一个子集,这样才是最大匹配算法对应的结果。

而对于原集合来说,最大匹配答案应该 /2。

AC代码(cpp)

class Solution {
    static const int N = 64;
    vector<int> G[N];
    bool used[N];
    int match[N];
    int r, c, n;
    int id(int row, int col) {
        return row * c + col;
    }
    void join(int x, int y) {
        G[x].push_back(y);
        G[y].push_back(x);
    }
    bool findPath(int u) {
        for (int v: G[u]) {
            if (used[v]) continue;
            used[v] = true;
            if (match[v] == -1 || findPath(match[v])) {
                match[v] = u;
                return true;
            }
        }
        return false;
    }
public:
    int maxStudents(vector<vector<char>>& seats) {
        r = seats.size(), c = seats[0].size(), n = r * c;
        int total = 0;

        for (int i = 0; i < r; i++) {
            for (int j = 0; j < c; j++) {
                if (seats[i][j] == '.') {
                    total++;
                    if (i > 0) {
                        if (j > 0 && seats[i - 1][j - 1] == '.') { // 左上
                            join(id(i, j), id(i - 1, j - 1));
                        }
                        if (j + 1 < c && seats[i - 1][j + 1] == '.') { // 右上
                            join(id(i, j), id(i - 1, j + 1));
                        }
                    }
                    if (j > 0 && seats[i][j - 1] == '.') { // 左侧
                        join(id(i, j), id(i, j - 1));
                    }
                }
            }
        }
        int matchNum = 0;
        memset(match, -1, sizeof match);
        for (int i = 0; i < n; i++) {
            memset(used, false, sizeof used);
            if (findPath(i)) ++matchNum;
        }

        return total - matchNum / 2;
    }
};
原文地址:https://www.cnblogs.com/wenruo/p/12357543.html