再谈位运算

本文深入探讨了位运算的基础知识,如与、或、非、异或等,并展示了如何利用这些运算在解决实际问题中的应用,如快速幂、大数乘法、最短Hamilton路径和图论中的异或操作。通过实例解析和代码示例,读者将理解如何在算法设计中巧妙运用位运算以优化时间和空间复杂度。
摘要由CSDN通过智能技术生成

再谈位运算


位运算基础:

基本运算:

与 : x & y只要有一个为 0 即为 0, 都为 1 为 1
或: x | y只要有一个为 1 即为 1, 都为 0 为 0
非 : ! x取反
异或 : x ^ y加法不进位, 相同为0, 不同为1

int 32位 :

1: 0000000000…1

2: 0000000000…10

3: 0000000000…11

补码: 一个数与 x 相加 为 0, 则这个数即为 x 的补码, 等于 x 取反 + 1, 也可以用 0 减去 x 得到

1 + x = 0000000000000000

1 + 1111111111111111111 = 0000000000…00

2 + 1111111111111111110 = 0000000000…00

x + ? = 000000000000…00

? 即为 x 的补码 :

求? : ? = ~x + 1

由此可得: -n = ~n + 1

优点 : 根据此原理在计算机中可以通过加法来实现减法

例子:

初始化数组为正无穷 : memset(f, 0x3f, sizeof (f) );

memset是按字节进行复制, int数组每个元素为 4 个字节, 故复制后每个元素为 0x3f3f3f3f

当数组中两个元素相加的时候, 0x3f3f3f3f * 2 < 0xffffffff

此时不会发生溢出错误

重要运算: 位运算

左移: 1 << n = 2 n 2^n 2n

右移: n >> x = n ÷ 2 x n \div {2^x} n÷2x 当右移的时候, 最左边的数被移除, 最高位补齐, 算术右移为补上它的符号位, 负数补1, 正数补0


例题解析:
1. a ^ b

求 a 的 b 次方对 p 取模的值。

输入格式

三个整数 a,b,p,在同一行用空格隔开。

输出格式

输出一个整数,表示a^b mod p的值。

数据范围

0 ≤ a , b ≤ 1 0 9 0 \le a,b \le {10^9} 0a,b109
1 ≤ p ≤ 1 0 9 1 \le p \le {10^9} 1p109

输入样例:

3 2 7

输出样例:

2

思路 : 快速幂, 将 b 拆分成二进制, 通过右移操作查看 b 的每一位, 同时 a 左移, 如果此位为 1 就乘上 a

**代码如下 : **

#include <iostream>

using namespace std ; 

int main() 
{
    int a, b, p ; cin >> a >> b >> p ; 
    int res = 1 % p ; // 这里 mod p 是为了防止 b 为 0, p 为 1 这组数据
    while (b) 
    {
        if (b & 1) res = res * 1ll * a % p ; // 这里乘 1ll 的目的是将res转换为长整型, 防止溢出
        a = a * 1ll * a % p ;
        b >>= 1 ; 
    }
    cout << res << endl ; 
    return 0 ; 
}
2. 64位整数乘法

求 a 乘 b 对 p 取模的值。

输入格式
第一行输入整数a,第二行输入整数b,第三行输入整数p。

输出格式
输出一个整数,表示a*b mod p的值。

数据范围
1≤a,b,p≤ 1 0 18 {10^{18}} 1018

输入样例:
3
4
5
输出样例:
2

思路: 快速幂思想, 两个64位的数相乘为128位, 必然溢出, 相加不溢出 --> 把乘法变成加法

a * b

a + a + a + … + a

a * 1 = a

a * 2 = 2a = a + a

a * 4= 4a = 2a + 2a

a * 8 = 8a = 4a + 4a

a * (2 ^ k) = 2^k * a

代码如下:

#include <iostream>

using namespace std ;

typedef unsigned long long ULL ;

int main()
{
    ULL a, b, p ;
    cin >> a >> b >> p ;
    ULL res = 0 ;
    while (b)
    {
        if (b & 1) res = (res + a) % p ;
        b >>= 1 ;
        a = (a + a) % p ; // a * 2 % p 也可
    }

    cout << res << endl ;

    return 0 ;
}
3.最短Hamilton路径 (NP完全问题)

旅行商问题

NP完全问题: NP完全问题(NP-C问题),是世界七大数学难题之一。 NP的英文全称是Non-deterministic Polynomial的问题,即多项式复杂程度的非确定性问题。简单的写法是 NP=P?,问题就在这个问号上,到底是NP等于P,还是NP不等于P。

因此只能通过暴力 (直接暴力会TLE) 优化的方法求解

给定一张 n 个点的带权无向图,点从 0∼n−1 标号,求起点 0 到终点 n−1 的最短 Hamilton 路径。

Hamilton 路径的定义是从 0 到 n−1 不重不漏地经过每个点恰好一次。

输入格式
第一行输入整数 n。

接下来 n 行每行 n 个整数,其中第 i 行第 j 个整数表示点 i 到 j 的距离(记为 a[i,j])。

对于任意的 x,y,z,数据保证 a[x,x]=0,a[x,y]=a[y,x] 并且 a[x,y]+a[y,z]≥a[x,z]。

输出格式
输出一个整数,表示最短 Hamilton 路径的长度。

数据范围
1≤n≤20
0≤a[i,j]≤ 1 0 7 10^7 107

输入样例:
5
0 2 4 5 1
2 0 6 5 3
4 6 0 8 3
5 5 8 0 5
1 3 3 5 0
输出样例:
18

思路:

假设遍历 0, 1, 2, 3这四个点

0 -> 1-> 2 -> 3 18

0 -> 1-> 2 -> 3 20

根据以上两种遍历方式可知, 当到第三个点的时候, 此时第一种方式的路径长度比第二种方式的少2

又因为从 3 号点遍历其他剩余的点的可能方式是相同的, 所以接下来第二种方式的每一种遍历方式都会比

第一种方式的遍历方式多 2, 因此 此时我们不需要再遍历第二种方式

两个关键点:

  1. 哪些点被用过;
  2. 目前停止哪个点上

第一维状态 : 2^20

第二维状态 : 20

数量 (时间复杂度) --> 2 ^ 20 * 20 = 2 * 10 ^ 7

考虑每个状态怎么被计算:

state表示哪些点被用过, j表示当前停在哪个点上

假设从 k 转移过来:

f[state] [j] = f[state_k] [k] + weight[k] [j], state_k = state除掉 j 之后的集合

集合表示当前状态已经遍历过的点

state_k 里面需要包含 k

状态压缩

用一个二进制的整数来表示我们的 state

是一个20位的整数 00…0

某一位为 1 表示这个点存在, 为 0 表示这个点不存在

假设现在 包含了 0, 1, 4 这三个点

state = 10011

整个程序复杂度 = 状态数量 * 状态转移复杂度

2 * 10 ^ 7 * 20

题目的 时/空限制:5s / 256MB 5s == 5亿次

我们这里程序的复杂度约为 4亿次 能够通过

代码如下:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std ; 

const int N = 20, M = 1 << 20 ; 

int n ; 
int f[M][N], weight[N][N] ; // 开数组到全局变量, 存到对空间中, 防止栈溢出 爆栈
                            // 一般较大数组都开为全局变量

int main()
{
    cin >> n ; 
    for (int i = 0;i < n;i ++ ) 
        for (int j = 0;j < n;j ++ ) 
            cin >> weight[i][j] ; 
        
    memset(f, 0x3f, sizeof f) ; 
    f[1][0] = 0 ; // 初始位置 
    
    for (int i = 0;i < 1 << n;i ++ ) 
        for (int j = 0;j < n;j ++ ) 
            if (i >> j & 1) // 判断当前状态是否合法, 判断 i 的第 j 位是否为 1
                for (int k = 0;k < n;k ++ ) 
                    if (i - (1 << j) >> k & 1)
                        f[i][j] = min(f[i][j], f[i - (1 << j)][k] + weight[k][j]) ; 
    
    cout << f[(1 << n) - 1][n - 1] << endl ; 
    return 0 ; 
}

异或 ^

  1. 实现 成对的 思想

0, 1

2, 3

4, 5

6, 7

0 ^ 1 = 1, 1 ^ 1 = 0

4 ^ 1 = 5, 5 ^ 1 = 4

也就是说将 每一个整数异或 1 即可得到它的配偶

作用: 图论中的最小费用流, 在用数组模拟邻接表存边的时候, 需要存一条边以及其反向边, 此时需要快速求出某一条边的反向边, 一般会将一条边的正向边与反向边放在一起

  1. lowbit运算

树状数组基础

快速求出一个整数 n 在二进制中最低的一位 1 是哪个

lowbit(11001000) = 1000

​ 00111000

int lowbit(n)
{
    return (-n) & n ; 
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值