前篇知识:
位运算得用途:
&运算
一个数 and 1的结果就是取二进制的最末位,用于判断数得奇偶性
一个数and 该数的负数,保留位中最右边 1
,且将其余的 1
设位 0
|运算
可以给二进制指定位置得值变成1,例如or 1,就是把最末位改成1,然后再减一就是0。这样可以这个数强行变成最接近的偶数。当然or (1 << n)就是末位得前 n - 1 位变成1。
^运算
两次异或同一个数最后结果不变,即(a xor b) xor b = a。这样可以用来加密文件,把a当作目标值,b当作密钥,c(a^b)为加密值
a xor a = 0;
0 xor a = a;
交换a,b值
a:=a xor b;
b:=a xor b;
a:=a xor b;
~运算
如果not的对象是无符号整数(不能表示负数),那么得到的值就是它与该类型上界的差,即-res = ~res + 1
<< 和 >>运算
<<二进制数向左移动多少位 >>向右移动多少位
或运算的最小翻转次数
题目:
给你三个正整数 a、b 和 c。
你可以对 a 和 b 的二进制表示进行位翻转操作,返回能够使按位或运算 a OR b == c 成立的最小翻转次数。
「位翻转操作」是指将一个数的二进制表示任何单个位上的 1 变成 0 或者 0 变成 1 。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路:
每个位置的求值之间没有存在联系,对比每个二进制各个位中得位数,不用管a,b,c变成多少,只要翻转得最小次数即可。
代码:
class Solution {
public:
int minFlips(int a, int b, int c) {
int res=0;
while(c!=0 || b!=0 || a!=0) { //易略点1
int n1=a%2,n2=b%2,n3=c%2;
if( (n1 | n2) != n3) { //难点1
if(n3 == 1) {
res++;
} else if(n1==0 || n2==0) {
res++;
} else {
res+=2;
}
}
a = a >> 1;
b = b >> 1;
c = c >> 1;
}
return res;
}
};
总结:
易略点1:这里我们对a,b,c进行判断,不能只对单一数进行判断就跳出循环,因为三个数得位数量可能不同
难点1:如果n1,n2之间没有括号,这时得优先级就不对,表达式中会先计算n2!=n3,再计算n1得或运算
只出现一次的数字 III
题目:
给定一个整数数组 nums
,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路:
能AC很简单,直接使用map即可。但是要用位运算就很难了
代码:
map方法:
class Solution {
public:
vector<int> singleNumber(vector<int>& nums) {
map<int,int> mii;
vector<int> res;
for(int i=0;i<nums.size();i++) {
mii[nums[i]]++;
}
map<int,int>::iterator iter;
for(iter=mii.begin();iter!=mii.end();iter++) {
if(iter->second == 1) {
res.push_back(iter->first);
}
if(res.size()==2) {
return res;
}
}
return res;
}
};
位运算法1:
//优化leetcode上的输入输出
static const auto _ = [](){
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
return nullptr;
}();
class Solution {
public:
vector<int> singleNumber(vector<int>& nums) {
int xorVal=0;
for(int num : nums) xorVal ^= num; //难点1
int diff = xorVal & (-xorval); //难点2
int a=0 , b=0;
for(int val : nums) {
//难点3
if(val & diff) a ^= val;
else b ^= val;
}
return {a,b};
}
};
位运算2:
class Solution {
public:
vector<int> singleNumber(vector<int>& nums) {
int res = 0;
for(int num : nums) res ^= num;
int x = 0, diff = res & (-res);
for(int num : nums) {
if(num & diff) x ^= num;
}
return vector<int>{x, res ^ x}; //难点4
}
};
总结:
map方法没啥好说的,就是map[数值]=出现次数,这个方法时间空间复杂度都比位运算大
难点1:题目中只存在2个不同数,同一个数 xor 该数为 0,0 xor 任何数为该数,所以xorVal值为2个不同数的异或运算值
难点2:求得xorVal中第一次出现1的位置,例如 xorVal = 276(1010100),那么diff = 0000100
难点3:用diff进行分类,假设两个不同数为a、b,val & diff中,a,b在diff位上的数一定是不同的,所以就可以进行分类。
res求出了两个唯一数的不同位数,我们以最右边出现1的位置来记作标识位 diff 来进行分类
难点4:第二种方法和第一个方法类似,就是使用的(a xor b) xor b = a 来求值,res为a ^ b ,只要求出a,即可得到b
得分最高的单词集合
题目:
你将会得到一份单词表 words,一个字母表 letters (可能会有重复字母),以及每个字母对应的得分情况表 score。
请你帮忙计算玩家在单词拼写游戏中所能获得的「最高得分」:能够由 letters 里的字母拼写出的 任意 属于 words 单词子集中,分数最高的单词集合的得分。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路:
1.回溯法:
使用递归思路,每个单词就是选择与不选的关系,然后我们就分别求出该单词选与不选的大小,对于后面的就使用递归函数计算,最后就是比较选与不选的大小进行返回
2.位压缩法:
规则:二进制位数为0,表示不使用该单词,反则使用。一共有(1 << words.size() )种情况,对于第i 种情况,使用 i & (1 << j )验证在i情况中,第 j 个单词有没有被使用到。用来求得这种情况符不符合题目条件
3.动态规划法:
这个就是最直观得方法,就是对于每个letter字典中有得字符都加上求值,若在中间发现不符合条件后,就再使用循环来回溯为原始状态。思路简单但是需要注意得细节很多
代码:
1.回溯法:
class Solution {
public:
int maxScoreWords(vector<string>& words, vector<char>& letters, vector<int>& score) {
vector<int> m(26);
for(int i=0;i<letters.size();i++) {
m[letters[i]-'a']++;
}
return backtrack(0 , m , words , score);
}
int backtrack(int index , vector<int> &m , vector<string>& words , vector<int>& score) {
if(index >= words.size()) return 0;
vector<int> t(m);
int ret = spell(t , words[index] , score);
int left = ret==0 ? 0 : ret + backtrack(index+1 , t , words , score);
int right = backtrack(index+1 , m , words , score);
return max(left , right);
}
int spell(vector<int> &t , string str , vector<int>& score) {
int res=0;
for(char c : str) {
if(!t[c - 'a']) return 0;
res += score[c - 'a'];
t[c - 'a']--;
}
return res;
}
};
2.位压缩法:
class Solution {
public:
vector<int> group(vector<string> &words , int bit) {
vector<int> g(26,0);
for(int i=0;i<words.size();i++) {
if(! (bit & (1 << i)) ) continue;
for(char c : words[i]) {
g[c - 'a']++;
}
}
return g;
}
int calaScore(vector<int> &group , vector<int> &letterscnt , vector<int>& score) {
int res=0;
for(int i=0;i<26;i++) {
if(letterscnt[i] < group[i]) return 0;
res += group[i] * score[i];
}
return res;
}
int maxScoreWords(vector<string>& words, vector<char>& letters, vector<int>& score) {
vector<int> letterscnt(26,0);
int res=0;
for(int i=0;i<letters.size();i++) {
letterscnt[letters[i] - 'a']++;
}
for(int i=0;i< (1 << words.size() ); i++) {
auto g = group(words , i);
res = max(res , calaScore(g , letterscnt , score));
}
return res;
}
};
3.动态规划法:
int lines[26],nums[26];
class Solution {
public:
int res=0;
int maxScoreWords(vector<string>& words, vector<char>& letters, vector<int>& score) {
//memset(lines, 0, sizeof(lines));
//memset(nums, 0, sizeof(nums));
for(int i=0;i<letters.size();i++) {
lines[letters[i] - 'a']++;
}
dfs(0 , score , words , 0);
return res;
}
void dfs(int p , vector<int> &score , vector<string> &words,int s) {
res = max(res , s);
while(p < words.size()) {
bool flag = true;
int news = s;
for(char c : words[p]) {
int ch = c - 'a';
news += score[ch];
if(++nums[ch] > lines[ch]) flag = false;
}
if(flag) {
dfs(p + 1 , score , words , news);
}
for(char c : words[p]) nums[c - 'a']--;
p++;
}
}
};
UTF-8 编码验证
题目:
UTF-8 中的一个字符可能的长度为 1 到 4 字节,遵循以下的规则:
对于 1 字节的字符,字节的第一位设为0,后面7位为这个符号的unicode码。
对于 n 字节的字符 (n > 1),第一个字节的前 n 位都设为1,第 n+1 位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。
输入是整数数组。只有每个整数的最低 8 个有效位用来存储数据。这意味着每个整数只表示 1 字节的数据。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/utf-8-validation
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路:
1.字符串法:
思路很简单,就是把十进制整数转化成二进制得字符串类型,然后在第一个数判断0得位置,以后得数判断是否以10开头即可
2.位运算法:
位运算就使用到了&操作来判断第一个数的头部有多少个1,然后一样用&操作判断以后得数判断是否以10开头。
代码:
1.字符串法:
class Solution {
public boolean validUtf8(int[] data) {
int res=0;
for(int n : data) {
//String bir = Integer.toBinayString(n);
String bir = Integer.toBinaryString(n);
bir = bir.length() >= 8 ? bir.substring(bir.length()-8) : "00000000".substring(8 - bir.length()) + bir; //易错点1
if( res == 0 ) {
for(int i=0;i<bir.length();i++) {
if(bir.charAt(i) == '0') break;
res++;
}
if(res == 0) continue; //易漏点1
if(res > 4 || res == 1) return false;
} else {
if(! (bir.charAt(0) == '1' && bir.charAt(1) == '0')) return false;
}
res--; //易错点2
}
return res == 0;
}
}
2.位运算法:
class Solution {
public:
bool validUtf8(vector<int>& data) {
int res = 0;
for(int n : data) {
if(res == 0) {
int mask1 = 1 << 7;
while(mask1 & n) {
res++;
mask1 >>= 1; //易错点1
}
if(res == 0) continue;
if(res > 4 || res == 1) return false;
} else {
//int mask1 = 1 << 7 , mask2 = 1 << 6; 优化
if( (!((n & (1 << 7)) != 0 && (n & (1 << 6)) == 0)) ) return false;
}
res--;
}
return res == 0;
}
};
总结:
第一个方法用的是java编写代码,因为java更加方便,有一些直接符合题意得源函数
1.字符串法:
易错点1:length()方法是针对字符串得,length属性是针对数组得。当不足8位时,需要在前面补0。补位数量也可以使用(bir.length() % 8)
易漏点1:边界情况:若数组以若干个0开头,直接跳过即可
易错点2:字节数得计算,不要忘记了在第一个数计算完后,也要减一。
2.位运算法:
易错点1:mask1对num从左到右判断1得数量,所以循环条件应该是mask>>1
插入
题目:
给定两个32位的整数N与M,以及表示比特位置的i与j。编写一种方法,将M插入N,使得M从N的第j位开始,到第i位结束。假定从j位到i位足以容纳M。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/insert-into-bits-lcci
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路:
只要把N 的 i到j位全部重置然后加上M << i 即可
代码:
class Solution {
public:
int insertBits(int N, int M, int i, int j) {
for(int k = i;k <= j; k++) {
if(N & (1 << k)) N -= (1 << k);
}
N += (M << i);
return N;
}
};
class Solution {
public:
int insertBits(int N, int M, int i, int j) {
int temp = (1 << (j - i + 1)) - 1 << i;
return (N & ~temp) | M << i;
}
};
总结:
1.我们先考虑会不会溢出的问题,例如1143207437 1017033 11 31,可以看出我们是在N位上替换成M的数,所以不存在溢出INT_MAX的情况。
2.把N 的 i到j位全部重置的方法,当然我们可以像第一个代码一样使用循环,用&运算判断是否为1,再减去即可。但是用"00..00"&N来重置更加快捷。
幂集
题目:
编写一种方法,返回某集合的所有子集。集合中不包含重复的元素。
思路:
1.回溯法:
num:记录一个集合的数量,以此为结束回溯的条件。每次都在上次回溯的基础上添加新的元素
2.位运算法:
一共有1<<nums.size()种情况,一样的每一个元素都是只有选与不选。用&运算来做选择条件
代码:
1.回溯法:
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
return backtrack(nums , 0);
}
vector<vector<int>> backtrack(vector<int> &nums , int num) {
if(num >= nums.size()) return {{}};
vector<vector<int>> ivec = backtrack(nums , num+1);
int sz = ivec.size();
for(int i=0; i<sz; i++) {
vector<int> vec = ivec[i];
vec.push_back(nums[num]);
ivec.push_back(vec);
}
return ivec;
}
};
2.位运算法:
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
const int sz = nums.size();
const int binCount = 1 << sz;
vector<vector<int>> res;
vector<int> vec;
for(int i=0; i<binCount; i++) {
for(int j=0;j<sz;j++)
if((i >> j) & 1) vec.push_back(nums[j]);
res.push_back(vec);
vec.clear();
}
return res;
}
};
总结:
回溯法没啥好说的,就是逻辑问题,什么时候以什么参数来完成回溯条件。
在回溯法转化位运算法中,都是用 1 << size 来记录一共有多少种情况,然后根据题意使用& 或者 | 运算来完成运算。
子数组按位或操作
题目:
我们有一个非负整数数组 A。
对于每个(连续的)子数组 B = [A[i], A[i+1], ..., A[j]] ( i <= j),我们对 B 中的每个元素进行按位或操作,获得结果 A[i] | A[i+1] | ... | A[j]。
返回可能结果的数量。 (多次出现的结果在最终答案中仅计算一次。)
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/bitwise-ors-of-subarrays
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路:
求出每个子集全部的或操作,直接暴力法简单,但是要使用剪枝技巧来提高效率。
代码:
class Solution {
public:
int subarrayBitwiseORs(vector<int>& A) {
int sz = A.size();
unordered_set<int> res;
if(sz < 2) return sz;
for(int i = 0; i < sz; i++) {
res.insert(A[i]);
for(int j = i - 1; j >= 0; j--) {
if((A[j] | A[i]) == A[j]) break; //剪枝条件
A[j] |= A[i];
res.insert(A[j]);
}
}
return res.size();
}
};
总结:
字母大小写全排列
题目:
给定一个字符串S
,通过将字符串S
中的每个字母转变大小写,我们可以获得一个新的字符串。返回所有可能得到的字符串集合。
思路:
在前面已经修改好了的基础上,每次改变一个字母,以达到递归的效果
代码:
class Solution {
public:
vector<string> letterCasePermutation(string S) {
vector<string> res{S};
for(int i = 0; i<S.size(); i++) {
if(isalpha(S[i])) {
for(int j = res.size()-1; j >= 0;j--) {
res.push_back(res[j]);
if(isupper(res[j][i])) res[j][i] = tolower(res[j][i]);
else res[j][i] = toupper(res[j][i]);
}
}
}
return res;
}
};
总结:
思路很简单,循环体的条件定义和什么时候添加string。这题与位运算好像没啥关系,但是这个代码编写一开始没有想到,所以也记录一下。