算法学习记录:位运算

前言:

  算法学习记录不是算法介绍,本文记录的是从零开始的学习过程(见到的例题,代码的理解……),所有内容按学习顺序更新,而且不保证正确,如有错误,请帮助指出。

学习工具:蓝桥OJ,LeetCode

目录

前言:

正文:

背景知识:

什么是位运算:

简单理解:

&:

|:

^:

<<: 

>>:

位运算的妙用:

1.判断数字奇偶性

2.获取二进制数的某一位

3.修改二进制数的某一位

4.快速判断一个数字是否为2的幂次方

5.获取二进制位中最低位1

用位运算解决问题(更新中):

1.快速求幂次:

蓝桥OJ 2230:快速幂

2.枚举子集:

蓝桥OJ 4360:串变换

蓝桥OJ 229:小明的宠物袋

3.倍增算法:

蓝桥OJ8617:LCA树上倍增

4.状压DP

蓝桥OJ 1261:小明的宠物袋


正文:

背景知识:

  你已经学过原码、反码、补码这样的基础知识。

  你可以进行进制换算。

  你了解过一定的程序的机器级表示。(CSAPP 第三章)

一些基础知识可以帮助理解,但不影响编程中位运算的学习。

什么是位运算:

数字在计算机中以二进制的形式存储,直接对二进制位进行的运算,就是位运算。

简单理解:

  C/C++的代码经过编译器转换和优化得到汇编代码。

因为计算机只能理解二进制数字,所以,十进制数字会被转换成二进制数字。

经过存储、运算等操作后,再变成人能看懂的十进制数字输出。

因此,位运算不仅可以提高效率,也有一些妙用,帮助我们解决复杂问题。

&:

与运算:”两个位都为1时,结果才为1“,默认是对最小的那一位,比如:

十进制数(7) = 二进制数(111):

8&1表示的意思就是:二进制111的最小位是不是1,111最小位是1,运算结果就是1( true)

|:

0或1,只要有一个1,运算结果就是1(true)。

^:

异或运算:”相同为1,相异为0“

<<: 

箭头向左,表示左移

对于有符号整数来说,这里的左移就是逻辑左移,x<<i相当于十进制乘以2^i

>>:

箭头向右,表示右移

对于有符号整数来说,这里的右移是逻辑右移,x>>i相当于十进制除以 2^i

位运算的妙用:

1.判断数字奇偶性

公式:x&1

2.获取二进制数的某一位

公式:x >> j & 1

3.修改二进制数的某一位

公式:

x || (1 << j)    修改位1

x & ~(1 << j)    修改为0

4.快速判断一个数字是否为2的幂次方

公式:x & (x - 1)

5.获取二进制位中最低位1

公式:lowbit(x) = x & - x

用位运算解决问题(更新中):

1.快速求幂次:
蓝桥OJ 2230:快速幂
/*倍增求快速幂*/

#include<bits/stdc++.h>
using namespace std;
#define mod 998244353
#define ll long long
int main(){
	ll b,p,k;
	cin >> b >> p >> k;
	ll s=1;
	while(p)
	{
		if(p&1)s = s * b % k;
		
		b = b * b % k;p >>= 1;
	}
	cout << s <<endl;
	
	return 0;
	
}

求 b ^ p % k ,一个十进制数总可以写成二进制,例如:求 2 ^ 7 ,下图可以帮助理解:

2.枚举子集:
蓝桥OJ 4360:串变换
#include<bits/stdc++.h>

using ll = long long;
using ld = long double;

using namespace std;
struct Q {
	int op,x,y;
};

void solve(const int &Case) {
	int n;cin >> n;
	string s,t;
	cin >> s >> t;
	int k;cin >> k;
	vector<Q>que(k);
	for(auto &[op,x,y]:que)cin >> op >> x >> y;
	for(int S = 0; S < 1<< k; S++) {// 枚举子集
		vector<int>tmp;
		for(int i = 0;i < k;i ++) {
			if(S >> i & 1)tmp.push_back(i);
		}
		do{
			string now = s;
			for(auto i :tmp) {
				const auto &[op,x,y] = que[i];
				if( op == 1) {
					s[x] = char('0' + (s[x] - '0' + y) % 10);
			}else {
					swap(s[x],s[y]);
				}
	    	}
			if(now == t) {
				cout << "Yes\n";
				return ;
			}	
			
    	}while(next_permutation(tmp.begin(),tmp.end()));  // 枚举排列
	}
    cout << "No\n";
}


int main() {
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	solve(1);
	
	return 0;
}

枚举子集通常和枚举排列一起使用。

分别是:每一个子元素的有无、每一个子元素的排列顺序 , 枚举出了它们的所有可能情况

枚举子集的代码也不好从直觉上理解,但是将原数转换成二进制再看会好理解的多。

蓝桥OJ 229:小明的宠物袋

状压DP中常用位运算的技巧 

// 对于第 i 层第 j 个格子能不能放宠物,我们肯定需要知道第 i - 1 层第 j 个格子有没有放宠物,且第 i 层第 j - 1 个格子有没有放宠物
// 状压,状态 T 的第 j 位表示上一层的第 j 个格子有没有放宠物,如果有是 1,没有就是 0,此时 S 就是 m 位二进制数
// f[i][T] 表示处理了前 i 层,第 i 层状态为 T 的最大宠物数量
// 我们枚举了当前层状态为 T,上一层状态为 S,根据上述信息,我们有以下几个限制:
// 1. 如果 S[j] = 1, 此时 T[j] 必须为 0,从二进制角度来看,就是不能有同一位同时为 1,即 T and S = 0
// 2. 如果 T[j] = 1, 此时 T[j + 1] 必须为 0,这启发我们预处理出合法的状态 T,即暴力枚举所有 T, 判断是否满足条件
// 3. 如果 a[i][j] = 1, 此时 T[j] 必须为 0,从二进制角度来看,就是不能有同一位同时为 1, 即 T and a[i] = 0
// 重新捋一下三个限制:
// 1. T and S = 0
// 2. T[j] and T[j + 1] = 0
// 3. T and a[i] = 0

#include <bits/stdc++.h>

using LL = long long;
using ld = long double;
using Pair = std::pair<int, int>;
#define inf 1'000'000'000'

void solve(const int &Case) {
    int n, m;
    std::cin >> n >> m;
    std::vector<int> a(n);
    for (int i = 0; i < n; i++) {
        int S = 0;
        for (int j = 0; j < m; j++) {
            int x;
            std::cin >> x;
            S = S * 2 | x;
        }
        a[i] = S;
    }
    std::vector<int> ban(1 << m);
    for (int S = 0; S < 1 << m; S++) { // 预处理出所有满足 T[j] and T[j + 1] = 0 的 T
        // ban[S] = 1 表示不满足
        for (int i = 0; i < m - 1; i++) {
            if ((S >> i & 1) && (S >> (i + 1) & 1)) {
                ban[S] = 1;
                break;
            }
        }
    }
    std::vector<int> f(1 << m, -1);
    f[0] = 0;
    for (int i = 0; i < n; i++) {
        auto g = f;
        std::vector<int>(1 << m, -1).swap(f);
        for (int T = 0; T < 1 << m; T++) {
            if (T & a[i] || ban[T])continue; // 合法的 T 要求 T and a[i] = 0 而且 ban[T] = 0
            // S and T = 0
            // (1 << m) - 1 为 m 位二进制数,且每一位都是 1
            // T xor ((1 << m) - 1) 等价于 T 每一位都取反, 即 0 变 1, 1 变 0
            // 此时 S and T = 0 等价于 S 是 T xor ((1 << m) - 1) 的子集
            // 然后枚举子集
            // __builtin_popcount(x) 表示的是 x 二进制位中 1 的个数,记不住这个函数可以自己提前预处理
            int S = T ^ ((1 << m) - 1), v = __builtin_popcount(T);
            for (int nS = S; nS > 0; nS = (nS - 1) & S) { // 枚举 nS 是 S 的子集,且按照字典序降序,建议记住
                if (g[nS] == -1)continue;
                f[T] = std::max(f[T], g[nS] + v);
            }
            f[T] = std::max(f[T], g[0] + v); // 注意上面的代码中,nS 不能等于 0,所以这里再特殊处理一下
        } // 这一串代码时间复杂度是 O(3 ^ m)
        // 参考位运算那一章节
    }
    std::cout << *std::max_element(f.begin(), f.end()) << '\n';
}

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
    int T = 1;
//    std::cin >> T;
    for (int Case = 1; Case <= T; Case++)solve(Case);
    return 0;
}
3.倍增算法:

这方面的用途广,会作为算法的代码实现方式出现。

蓝桥OJ8617:LCA树上倍增
#include <bits/stdc++.h>

using LL = long long;
using Pair = std::pair<int, int>;
#define inf 1'000'000'000'

void solve(const int &Case) {
    int n;
    std::cin >> n;
    std::vector<std::vector<int>> G(n + 1);
    for (int i = 1; i < n; i++) {
        int u, v;
        std::cin >> u >> v;
        G[u].push_back(v), G[v].push_back(u);
    }
    std::vector<std::array<int, 21>> F(n + 1);
    std::vector<int> dep(n + 1);
    std::function<void(int, int)> dfs = [&](int x, int fax) {
        F[x][0] = fax;
        for (int i = 1; i <= 20; i++)F[x][i] = F[F[x][i - 1]][i - 1];
        for (const auto &tox: G[x]) {
            if (tox == fax)continue;
            dep[tox] = dep[x] + 1;
            dfs(tox, x);
        }
    };
    dfs(1, 0);
    auto glca = [&](int x, int y) {
        if (dep[x] < dep[y])std::swap(x, y);
        int d = dep[x] - dep[y];
        for (int i = 20; i >= 0; i--)if (d >> i & 1)x = F[x][i];
        if (x == y)return x;
        for (int i = 20; i >= 0; i--) {
            if (F[x][i] != F[y][i]) {
                x = F[x][i];
                y = F[y][i];
            }
        }
        return F[x][0];
    };
    int q;
    std::cin >> q;
    while (q--) {
        int x, y;
        std::cin >> x >> y;
        std::cout << glca(x, y) << '\n';
    }
}

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
    int T = 1;
    for (int Case = 1; Case <= T; Case++)solve(Case);
    return 0;
}
4.状压DP

综合运用了各种位运算技巧

蓝桥OJ 1261:小明的宠物袋
// 对于第 i 层第 j 个格子能不能放宠物,我们肯定需要知道第 i - 1 层第 j 个格子有没有放宠物,且第 i 层第 j - 1 个格子有没有放宠物
// 状压,状态 T 的第 j 位表示上一层的第 j 个格子有没有放宠物,如果有是 1,没有就是 0,此时 S 就是 m 位二进制数
// f[i][T] 表示处理了前 i 层,第 i 层状态为 T 的最大宠物数量
// 我们枚举了当前层状态为 T,上一层状态为 S,根据上述信息,我们有以下几个限制:
// 1. 如果 S[j] = 1, 此时 T[j] 必须为 0,从二进制角度来看,就是不能有同一位同时为 1,即 T and S = 0
// 2. 如果 T[j] = 1, 此时 T[j + 1] 必须为 0,这启发我们预处理出合法的状态 T,即暴力枚举所有 T, 判断是否满足条件
// 3. 如果 a[i][j] = 1, 此时 T[j] 必须为 0,从二进制角度来看,就是不能有同一位同时为 1, 即 T and a[i] = 0
// 重新捋一下三个限制:
// 1. T and S = 0
// 2. T[j] and T[j + 1] = 0
// 3. T and a[i] = 0

#include <bits/stdc++.h>

using LL = long long;
using ld = long double;
using Pair = std::pair<int, int>;

void solve(const int &Case) {
    int n, m;
    std::cin >> n >> m;
    std::vector<int> a(n);
    for (int i = 0; i < n; i++) {
        int S = 0;
        for (int j = 0; j < m; j++) {
            int x;
            std::cin >> x;
            S = S * 2 | x;
        }
        a[i] = S;
    }
    std::vector<int> ban(1 << m);
    for (int S = 0; S < 1 << m; S++) { // 预处理出所有满足 T[j] and T[j + 1] = 0 的 T
        // ban[S] = 1 表示不满足
        for (int i = 0; i < m - 1; i++) {
            if ((S >> i & 1) && (S >> (i + 1) & 1)) {
                ban[S] = 1;
                break;
            }
        }
    }
    std::vector<int> f(1 << m, -1);
    f[0] = 0;
    for (int i = 0; i < n; i++) {
        auto g = f;
        std::vector<int>(1 << m, -1).swap(f);
        for (int T = 0; T < 1 << m; T++) {
            if (T & a[i] || ban[T])continue; // 合法的 T 要求 T and a[i] = 0 而且 ban[T] = 0
            // S and T = 0
            // (1 << m) - 1 为 m 位二进制数,且每一位都是 1
            // T xor ((1 << m) - 1) 等价于 T 每一位都取反, 即 0 变 1, 1 变 0
            // 此时 S and T = 0 等价于 S 是 T xor ((1 << m) - 1) 的子集
            // 然后枚举子集
            // __builtin_popcount(x) 表示的是 x 二进制位中 1 的个数,记不住这个函数可以自己提前预处理
            int S = T ^ ((1 << m) - 1), v = __builtin_popcount(T);
            for (int nS = S; nS > 0; nS = (nS - 1) & S) { // 枚举 nS 是 S 的子集,且按照字典序降序,建议记住
                if (g[nS] == -1)continue;
                f[T] = std::max(f[T], g[nS] + v);
            }
            f[T] = std::max(f[T], g[0] + v); // 注意上面的代码中,nS 不能等于 0,所以这里再特殊处理一下
        } // 这一串代码时间复杂度是 O(3 ^ m)
        // 参考位运算那一章节
    }
    std::cout << *std::max_element(f.begin(), f.end()) << '\n';
}

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);
    int T = 1;
//    std::cin >> T;
    for (int Case = 1; Case <= T; Case++)solve(Case);
    return 0;
}

  • 52
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值