算法【状压dp】

状压dp是指设计一个整型可变参数status,利用status的位信息,来表示某个样本是否还能使用,然后利用这个信息进行尝试。写出尝试的递归函数 -> 记忆化搜索 -> 严格位置依赖的动态规划 -> 空间压缩等优化。

如果有k个样本,那么表示这些样本的状态,数量是2^k。所以可变参数status的范围: 0 ~ (2^k)-1

样本每增加一个,状态的数量是指数级增长的,所以状压dp能解决的问题往往样本数据量都不大。一般样本数量在20个以内(10^6),如果超过这个数量,计算量(指令条数)会超过 10^7 ~ 10^8

如果样本数量大到状压dp解决不了,或者任何动态规划都不可行,那么双向广搜是一个备选思路

下面通过题目加深理解。

题目一

测试链接:464. 我能赢吗 - 力扣(LeetCode)

分析:因为可选择的数不超过20,所以采用状压dp。用一个state的位信息表示一个数取或没取。可能性的展开就是,遍历数字,如果可以取并且取完这个数后另一个玩家是必输的,则当前玩家是必赢的。下面是记忆化搜索的版本,代码如下。

class Solution {
public:
    int dp[(1 << 21)] = {0};
    bool f(int state, int rest, int maxChoosableInteger){
        if(rest <= 0){
            return false;
        }
        if(dp[state] != 0){
            return dp[state] == 1;
        }
        dp[state] = -1;
        for(int i = 1;i <= maxChoosableInteger;++i){
            if((state & (1 << i)) && !f(state ^ (1 << i), rest-i, maxChoosableInteger)){
                dp[state] = 1;
                break;
            }
        }
        return dp[state] == 1;
    }
    bool canIWin(int maxChoosableInteger, int desiredTotal) {
        if(desiredTotal == 0){
            return true;
        }
        if((1 + maxChoosableInteger) * maxChoosableInteger / 2 < desiredTotal){
            return false;
        }
        return f((1 << (maxChoosableInteger+1))-1, desiredTotal, maxChoosableInteger);
    }
};

其中,f方法返回在state状态和还剩rest就达到界限的情况下当前玩家是否稳赢。

题目二

测试链接:473. 火柴拼正方形 - 力扣(LeetCode)

分析:因为火柴的个数不超过15,所以也可以采用状压dp,用state的位信息表示火柴棒取或没取,只需要逐个拼完四条边就代表能够拼成一个正方形。下面是记忆化搜索的版本,代码如下。

class Solution {
public:
    int dp[(1 << 15)] = {0};
    bool f(int state, int rest, int nums, vector<int>& matchsticks, int edge){
        if(nums == 5){
            return true;
        }
        if(dp[state] != 0){
            return dp[state] == 1;
        }
        dp[state] = -1;
        for(int i = 0;i < matchsticks.size();++i){
            if((state & (1 << i)) && matchsticks[i] <= rest &&
            f(state ^ (1 << i), rest-matchsticks[i] == 0 ? edge : rest-matchsticks[i], rest-matchsticks[i] == 0 ? nums+1 : nums, matchsticks, edge)){
                dp[state] = 1;
                break;
            }
        }
        return dp[state] == 1;
    }
    bool makesquare(vector<int>& matchsticks) {
        int sum = 0;
        for(int i = 0;i < matchsticks.size();++i){
            sum += matchsticks[i];
        }
        int edge = sum / 4;
        if(edge * 4 != sum){
            return false;
        }
        return f((1 << (matchsticks.size()))-1, edge, 1, matchsticks, edge);
    }
};

其中,f方法返回在state状态下,当前边还剩rest,这是第nums条边,每条边长度edge的情况下能否拼成一个正方形。

题目三

测试链接:698. 划分为k个相等的子集 - 力扣(LeetCode)

分析:这道题和上一道题思路差不多。下面是记忆化搜索的版本,代码如下。

class Solution {
public:
    int dp[(1 << 16)] = {0};
    bool f(int state, int every, int k, int rest, vector<int>& nums, int length, int seq){
        if(seq == k+1){
            return true;
        }
        if(dp[state] != 0){
            return dp[state] == 1;
        }
        dp[state] = -1;
        for(int i = 0;i < length;++i){
            if((state & (1 << i)) && rest-nums[i] >= 0 &&
            f(state ^ (1 << i), every, k, rest-nums[i] == 0 ? every : rest-nums[i], nums, length, rest-nums[i] == 0 ? seq+1 : seq)){
                dp[state] = 1;
                break;
            }
        }
        return dp[state] == 1;
    }
    bool canPartitionKSubsets(vector<int>& nums, int k) {
        int sum = 0;
        int length = nums.size();
        for(int i = 0;i < length;++i){
            sum += nums[i];
        }
        int every = sum / k;
        if(every * k != sum){
            return false;
        }
        return f((1 << length)-1, every, k, every, nums, length, 1);
    }
};

其中,f方法返回在state状态,每个子集和为every,共k个子集,当前子集还差rest,当前是第seq个子集的情况下能够划分完成。

题目四

测试链接:售货员的难题 - 洛谷

分析:这是一个TSP问题,但是因为村庄的个数不超过20,所以可以采用状压dp求解,用state的位信息表示一个村庄是否经过。下面是记忆化搜索的版本,代码如下。

#include <iostream>
using namespace std;
int n;
int path[20][20] = {0};
int dp[(1 << 20)][20] = {0};
int f(int state, int during){
    if(state == 0){
        return path[during][0];
    }
    if(dp[state][during] != 0){
        return dp[state][during];
    }
    dp[state][during] = -((1 << 31) + 1);
    for(int i = 1;i < n;++i){
        if((state & (1 << i))){
            dp[state][during] = min(dp[state][during], path[during][i]+f(state ^ (1 << i), i));
        }
    }
    return dp[state][during];
}
int main(void){
    scanf("%d", &n);
    for(int i = 0;i < n;++i){
        for(int j = 0;j < n;++j){
            scanf("%d", &path[i][j]);
        }
    }
    printf("%d", f((1 << n)-2, 0));
    return 0;
}

其中,f函数返回在state状态,当前村庄为during的情况下完成目标的最短路径。

题目五 

测试链接:1434. 每个人戴不同帽子的方案数 - 力扣(LeetCode)

分析:这道题帽子的数量超过了20,所以state并不能用来表示帽子是否被使用。观察到人的个数不超过10,所以state的位信息用来表示人是否被满足。可能性的展开就是在state状态时,对于第i个帽子要或不要。下面是记忆化搜索的版本,代码如下。

class Solution {
public:
    int dp[(1 << 10)][41];
    int MOD = 1000000007;
    int f(int state, int color, vector<vector<int>>& hats, int person, int max_hat){
        if(state == 0){
            return 1;
        }
        if(color > max_hat){
            return 0;
        }
        if(dp[state][color] != -1){
            return dp[state][color];
        }
        int ans = f(state, color+1, hats, person, max_hat);
        for(int i = 0;i < person;++i){
            if((state & (1 << i))){
                for(int j = 0;j < hats[i].size();++j){
                    if(color == hats[i][j]){
                        ans = (ans + f(state ^ (1 << i), color+1, hats, person, max_hat)) % MOD;
                    }
                }
            }
        }
        dp[state][color] = ans;
        return ans;
    }
    int numberWays(vector<vector<int>>& hats) {
        int person = hats.size();
        int max_hat = 0;
        for(int i = 0;i < person;++i){
            for(int j = 0;j < hats[i].size();++j){
                max_hat = max(max_hat, hats[i][j]);
            }
        }
        for(int i = 0;i < (1 << person);++i){
            for(int j = 1;j <= 40;++j){
                dp[i][j] = -1;
            }
        }
        return f((1 << person)-1, 1, hats, person, max_hat);
    }
};

其中,f方法返回在state状态时,从color帽子开始能否满足所有人。

题目六

测试链接:1994. 好子集的数目 - 力扣(LeetCode)

分析:这道题需要观察到nums中的值不超过30,而不超过30的质数总共为10个(2,3,5,7,11,13,17,19,23,29)对于这十个质数做状态压缩。就是对于这10个质数所组成的不同状态产生有多少种,把所有状态的总数累加起来就是答案。因为0到30个数规模较小,可以直接用一个表结构缩短时间即对每一个数用一个10位的状态表示,每一位代表一个质数,如果它不能分解为不同的质因子,这个数为零;如果可以则相应分解的质因子的位为1。下面是记忆化搜索的版本,代码如下。

class Solution {
public:
    int dp[(1 << 10)][31];
    int table[31] = {
        0b0000000000,
        0b0000000000,
        0b0000000001,
        0b0000000010,
        0b0000000000,
        0b0000000100,
        0b0000000011,
        0b0000001000,
        0b0000000000,
        0b0000000000,
        0b0000000101,
        0b0000010000,
        0b0000000000,
        0b0000100000,
        0b0000001001,
        0b0000000110,
        0b0000000000,
        0b0001000000,
        0b0000000000,
        0b0010000000,
        0b0000000000,
        0b0000001010,
        0b0000010001,
        0b0100000000,
        0b0000000000,
        0b0000000000,
        0b0000100001,
        0b0000000000,
        0b0000000000,
        0b1000000000,
        0b0000000111
    };
    int times[31] = {0};
    int MOD = 1000000007;
    int one = 1;
    int f(int state, int i){
        if(state == 0){
            return one;
        }
        if(i < 2){
            return 0;
        }
        if(dp[state][i] != -1){
            return dp[state][i];
        }
        int ans = f(state, i-1);
        if(table[i] != 0 && (state | table[i]) == state && times[i] != 0){
            ans = (int)((ans + (long long)f(state ^ table[i], i-1) * times[i]) % MOD);
        }
        dp[state][i] = ans;
        return ans;
    }
    int numberOfGoodSubsets(vector<int>& nums) {
        int length = nums.size();
        int max_num = 0;
        for(int i = 0;i < length;++i){
            ++times[nums[i]];
            max_num = max(max_num, nums[i]);
        }
        for(int i = 0;i < (1 << 10);++i){
            for(int j = 0;j <= max_num;++j){
                dp[i][j] = -1;
            }
        }
        for(int i = 0;i < times[1];++i){
            one = (one << 1) % MOD;
        }
        int ans = 0;
        for(int i = 1;i < (1 << 10);++i){
            ans = (ans + f(i, max_num)) % MOD;
        }
        return ans;
    }
};

其中,f方法返回在状态state,从第i个数开始的情况下,产生状态state的种数。

题目七

测试链接:1655. 分配重复整数 - 力扣(LeetCode)

分析:这道题观察到顾客不超过10人所以对顾客采用状态压缩。首先做一个词频统计,然后可能性的展开即对第i个数要或不要,要的话可以满足哪些顾客,这里并不需要将i用完,只需遍历i能满足哪些顾客的种数即可。下面是记忆化搜索的版本,代码如下。

class Solution {
public:
    int times[50] = {0};
    int dp[(1 << 10)][50] = {0};
    int table[(1 << 10)] = {0};
    bool f(int state, int i, vector<int>& quantity, int number_num){
        if(state == 0){
            return true;
        }
        if(i == number_num){
            return false;
        }
        if(dp[state][i] != 0){
            return dp[state][i] == 1;
        }
        int ans = -1;
        for(int j = state;j > 0;j = ((j - 1) & state)){
            if(times[i] >= table[j] && f(state ^ j, i+1, quantity, number_num)){
                ans = 1;
                break;
            }
        }
        if(ans == -1){
            ans = f(state, i+1, quantity, number_num) ? 1 : -1;
        }
        dp[state][i] = ans;
        return ans == 1;
    }
    bool canDistribute(vector<int>& nums, vector<int>& quantity) {
        sort(nums.begin(), nums.end());
        int length = nums.size();
        int person = quantity.size();
        int index = 0;
        int number_num = 0;
        int num;
        while (index < length)
        {
            num = 1;
            while (index < length-1 && nums[index] == nums[index+1])
            {
                ++index;
                ++num;
            }
            times[number_num++] = num;
            ++index;
        }
        int sum;
        for(int i = 1;i < (1 << person);++i){
            sum = 0;
            for(int j = 0;j < person;++j){
                if(((i >> j) & 1) != 0){
                    sum += quantity[j];
                }
            }
            table[i] = sum;
        }
        return f((1 << person)-1, 0, quantity, number_num);
    }
};

其中,table数组存储每一个数相应位代表的顾客被满足需要多少个相同的数;f方法返回在状态state,从下标为i的数开始的情况下能否满足所有顾客。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

还有糕手

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值