总结
二进制位运算可以用来枚举,dp状态压缩的时候也可以派上用场。还有像是树状数组就是基于位运算。
考虑用二进制表示枚举状态的情况,有这几个特点,(1)对每个对象可以写成两种状态(2)对象数目不多,一般16个以内(3)需要枚举这几个对象任意状态的组合
题解
78. 子集
对有n个数的的数组,他的子集个数应该是 2 n 2^n 2n,在 [ 0 , 2 n ) [0,2^n) [0,2n)中遍历 i i i,不难发现,将数组中每个数字对应到 i i i 的二进制表示。假设 i i i 为5,即101,那么第 i i i个集合中放入数组中第0和第2个数字。可以发现,由于 i i i 不重复,得到的也正是不重复的 2 n 2^n 2n的集合。
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
int n = nums.size();
vector<vector<int>> ans;
vector<int> t;
for(int mask = 0; mask < (1 << n); ++ mask){
t.clear();
for(int i = 0; i < n; ++ i){
if(mask & (1 << i))
t.push_back(nums[i]);
}
ans.push_back(t);
}
return ans;
}
};
90. 子集 II
对于有重复元素的,先对数组排序,然后要去除的是,上一个数字跟现在数字相同,且上一个数字不在集合中。
class Solution {
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
int n = nums.size();
sort(nums.begin(), nums.end());
vector<int> t;
vector<vector<int>> ans;
for(int mask = 0; mask < (1 << n); ++ mask){
t.clear();
bool flag = true;
for(int i = 0; i < n; ++ i){
if(mask & (1 << i)){
if(i > 0 && nums[i-1] == nums[i] && (mask & (1 << (i-1)))== 0){
flag = false;
break;
}
t.push_back(nums[i]);
}
}
if(flag)
ans.push_back(t);
}
return ans;
}
};
1434. 每个人戴不同帽子的方案数
这题暴力回溯的话会超时。先来看看暴搜的思路。遍历每个人喜欢的帽子,用一个visit数组存储该序号的帽子是否被人戴了,没有的话就当前人戴上,遍历下一个。遍历到最后一个人的时候方案数就加一。
这样暴搜会超时,想想怎么剪枝?
先对比一下重复运算在哪一块。其实递归只用到两个状态,(1)每个位置的帽子是否有人戴了(2)每个人是否有戴帽子。
假设1号2号人都喜欢a号b号帽子,那么1戴上a,2戴上b与1戴上b,2戴上a这两种情况,对于其他人而言,这个时候其实是重复运算的。那其实我们要有的就是1,2戴上帽子,ab帽子也都被使用的状态下方法数。
那怎么存储这两种状态?考虑到有一个可以放在循环中隐式存储,只用考虑一种状态,发现n是小于10,用n位的二进制表示每个位置的人是否戴上帽子不就可以了!!
class Solution {
public:
const int mod = 1e9+7;
int res = 0;
void dfs(vector<vector<int>> &hats, vector<bool>& visit, int cur){
if(cur == hats.size()){
++ res;
return;
}
for(int i = 0; i < hats[cur].size(); ++ i){
if(visit[hats[cur][i]] == false){
visit[hats[cur][i]] = true;
dfs(hats, visit, cur+1);
visit[hats[cur][i]] = false;
}
}
}
int numberWays(vector<vector<int>>& hats) {
int n = hats.size(), m = 0;
for(int i = 0; i < hats.size(); ++ i){
for(int h : hats[i])
m = max(h, m);
}
vector<bool> visit(m+1, false);
dfs(hats, visit, 0);
return res;
}
};
用 d p [ i ] [ j ] dp[i][j] dp[i][j]表示用了前 [ 1 , i ] [1,i] [1,i]个帽子(不一定这些都被戴上),然后人戴帽子的二进制状态为 j j j此时的方案数。
class Solution {
public:
const int mod = 1e9+7;
int numberWays(vector<vector<int>>& hats) {
int n = hats.size(), m = 0;
for(int i = 0; i < hats.size(); ++ i){
for(int h : hats[i])
m = max(h, m);
}
vector<vector<int>> hatsToperson(m+1);
for(int i = 0; i < hats.size(); ++ i){
for(int h : hats[i])
hatsToperson[h].push_back(i);
}
vector<vector<int>> dp(m+1, vector<int>(1 << n, 0));
dp[0][0] = 1;
for(int i = 1; i <= m; ++ i){
for(int j = 0; j < (1 << n); ++ j){
dp[i][j] = dp[i-1][j];
for(int h : hatsToperson[i]){
if(j & (1 << h)){
dp[i][j] += (dp[i-1][j^(1<<h)]);
dp[i][j] %= mod;
}
}
}
}
return dp[m][(1<<n)-1];
}
};
当然老规矩,可以压缩成一维的。
class Solution {
public:
const int mod = 1e9+7;
int numberWays(vector<vector<int>>& hats) {
int n = hats.size(), m = 0;
for(int i = 0; i < hats.size(); ++ i){
for(int h : hats[i])
m = max(h, m);
}
vector<vector<int>> hatsToperson(m+1);
for(int i = 0; i < hats.size(); ++ i){
for(int h : hats[i])
hatsToperson[h].push_back(i);
}
vector<int> dp(1 << n, 0);
dp[0] = 1;
for(int i = 1; i <= m; ++ i){
for(int j = (1 << n)-1; j >= 0; -- j){
for(int h : hatsToperson[i]){
if(j & (1 << h)){
dp[j] += (dp[j^(1<<h)]);
dp[j] %= mod;
}
}
}
}
return dp[(1<<n)-1];
}
};
1125. 最小的必要团队
技能数在16以内,考虑用二进制表示每个技能是否达到了。
不过这里的dp的时候,跟之前不太一样,一般来讲需要改变的当前遍历到的dp位置,但是这里对每个人来说,其实受影响的位置是,这个人拥有的技能都点满的那些状态。所以这里其实改变的只是(j|mask),j表示当前遍历到的状态,mask表示的是这个人拥有的技能状态。
如果想改变当前状态的dp的话,需要比较当前状态去除这个人拥有的某些技能的dp,这里不方便写代码。
class Solution {
public:
vector<int> smallestSufficientTeam(vector<string>& req_skills, vector<vector<string>>& people) {
int n = req_skills.size(), m = people.size();
unordered_map<string,int> mp;
for(string req_skill : req_skills)
mp[req_skill] = mp.size();
vector<int> dp(1<<n, 0x3f3f3f3f);
vector<vector<int>> team(1<<n);
dp[0] = 0;
for(int i = 1; i <= m; ++ i){
int mask = 0;
for(string skill : people[i-1])
mask |= (1 << mp[skill]);
for(int j = (1<<n) - 1; j >= 0; -- j){
int x = j | mask;
if(dp[x] > dp[j] + 1){
team[x] = team[j];
team[x].push_back(i-1);
dp[x] = dp[j]+1;
}
}
}
return team[(1<<n)-1];
}
};
847. 访问所有节点的最短路径
这题题意是遍历完所有的节点经历的最短路径,可以从任意点开始。节点数目在12以内。
最短路径想到BFS
任意点开始想到多源BFS
节点数目12以内想到状态压缩,二进制表示状态
本题就是多源BFS+状态压缩
class Solution {
public:
int shortestPathLength(vector<vector<int>>& graph) {
int n = graph.size();
vector<vector<int>> visit(n, vector<int>(1 << n, -1));
queue<pair<int,int>> q;
for(int i = 0; i < n; ++ i){
q.emplace(i, 1 << i);
visit[i][1<<i] = 0;
}
int ans = 0;
while(!q.empty()){
auto [u, mask] = q.front();
q.pop();
if(mask == (1 << n) - 1){
ans = visit[u][mask];
break;
}
int dist = visit[u][mask];
for(int v : graph[u]){
int mask_v = mask | (1 << v);
if(visit[v][mask_v] == -1){
q.emplace(v, mask_v);
visit[v][mask_v] = dist+1;
}
}
}
return ans;
}
};