算法進階指南0.1 位運算

常見的位運算

&、或 |、非、異或^
左移 >> 、右移<<

補碼

首先我們來簡單介紹一下 原碼、反碼、補碼的概念:

1. 原碼

原碼就是符號位加上真值的絕對值, 即用第一位表示符號, 其餘位表示值. 比如如果是8位二進制:
.

 [+1]= 0000 0001 
 [-1]= 1000 0001

2. 反碼

反碼的表示方法是:

  1. 正數的反碼是其本身
  2. 負數的反碼是在其原碼的基礎上, 符號位不變,其餘各個位取反.
[+1] = [00000001]= [00000001][-1] = [10000001]= [11111110]

可見如果一個反碼錶示的是負數, 人腦無法直觀的看出來它的數值. 通常要將其轉換成原碼再計算.

3. 補碼

補碼的表示方法是:

  1. 正數的補碼就是其本身
  2. 負數的補碼是在其原碼的基礎上, 符號位不變, 其餘各位取反, 最後+1. (即在反碼的基礎上+1)
[+1] = [00000001]= [00000001]= [00000001][-1] = [10000001]= [11111110]= [11111111]

按位取反運算符~

二進制數在內存中以補碼的形式存儲。

按位取反:二進制每一位取反,0變1,1變0。

~9的計算步驟:

轉二進制:0 1001

計算補碼:0 1001

按位取反:1 0110

轉爲原碼:

按位取反:1 1001

末位加一:1 1010

符號位爲1是負數,即-10

求負數運算:

-x = ~x + 1

如:

1 表示爲 00000…01. , ~1 表示爲:11111…10, ~1 +1表示爲 :11111…11

 00000...01 + 11111...11 = 00000..00

例題

89. a^b

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

輸入格式: 三個整數 a,b,p , 在同一行用空格隔開。

輸出格式:輸出一個整數,表示a^b mod p的值。

數據範圍:0a,b,p1090≤a,b,p≤10^9

首先我們想到用循環暴力法:

import java.io.*;
import java.util.*;

public class Main {
    public static void main(String args[]) throws Exception {
        Scanner cin=new Scanner(System.in);
        int a = cin.nextInt();
        int b = cin.nextInt();
        int p = cin.nextInt();
        int res = 1;
        
        while (b > 0) {
            res = res * a;
            res %= mod;
        }
        System.out.println(res);
    }
}

但是我們可以看到a, b 的取值範圍爲 0≤a,b,p≤109, 所以循環暴力法無疑是超時的。這就需要我們用到快速冪的思想。可將時間複雜度降低至 O(log2n)。以下從 “二分法” 和 “二進制” 兩個角度解析快速冪法。

在這裏插入圖片描述
在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述

在這裏插入圖片描述
快速冪模版

int qmi(int m, int k, int p)
{
    int res = 1 % p, t = m;
    while (k)
    {
        if (k&1) res = res * t % p;
        t = t * t % p;
        k >>= 1;
    }
    return res;
}
import java.io.*;
import java.util.*;

public class Main {
    public static void main(String args[]) throws Exception {
        Scanner cin=new Scanner(System.in);
        /*
			res, a 可能會溢出,所以修改爲long格式
		*/
        long a = cin.nextInt();
        long b = cin.nextInt();
        int p = cin.nextInt();
        long res = 1;
        
        while (b > 0) {
            if ((b & 1) == 1) {
                res *= a;
                res %= p;
            }
            a *= a;
            a %= p;
            b >>= 1;
        }
        res %= p;
        System.out.println((int)res);
    }
}

例二:64位整數乘法

求 a 乘 b 對 p 取模的值。

輸入格式: 第一行輸入整數a,第二行輸入整數b,第三行輸入整數p。

輸出格式 :輸出一個整數,表示a*b mod p的值。

數據範圍 : 1a,b,p10181≤a,b,p≤10^{18}

輸入樣例:
3
4
5
輸出樣例:

2

這道題我們首先想到的是直接乘,簡單方便

import java.util.*;
public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        long a = sc.nextLong();
        long b = sc.nextLong();
        long p = sc.nextLong();

        System.out.println(a * b % p);
    }
}

但是我們知道數值類型long信息如下:

	long 二進制位數:64
	包裝類:java.lang.Long
	最小值:Long.MIN_VALUE=-9223372036854775808 (-2的63次方)
	最大值:Long.MAX_VALUE=9223372036854775807 (2的63次方-1)

int 範圍大約爲 29-2^{9}long範圍大約爲218-2^{18},所以直接計算會導致溢出,這就需要我們使用 a + a + a + … + a的方法,因此我們也可以用快速冪思想:

ab=a(b020+b121+...bn2n)a * b = a*(b_{0}*2^{0}+b_{1}*2^{1}+...b_{n}*2^{n})

import java.util.*;
public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        long a = sc.nextLong();
        long b = sc.nextLong();
        long p = sc.nextLong();
        long res = 0;
        
        while (b > 0) {
            if ((b & 1) == 1) {
                res = (res + a) % p;
            }
            b >>= 1;
            a = (a + a) % p;
        }
        
        System.out.println(res);
    }
}

例3: 最短Hamilton路徑

給定一張 n 個點的帶權無向圖,點從 0~n-1 標號,求起點 0 到終點 n-1 的最短Hamilton路徑。 Hamilton路徑的定義是從 0 到 n-1 不重不漏地經過每個點恰好一次。

題目描述

給定一張 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]≤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

第一解決思路當然是純暴力枚舉所有路徑,找出最小的。如下使用回溯法

import java.util.*;
class Main {
    static long res = Integer.MAX_VALUE;
    static boolean[] mark;
    static int[][] d;
    
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int n = in.nextInt();
        mark = new boolean[n];
        mark[0] = true;
        d = new int[n][n];
        
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                d[i][j] = in.nextInt();
            }
        }
        for (int i = 1; i < n - 1; i++) {
            mark[i] = true;
            dfs(0, i, d[0][i], 1, n);
            mark[i] = false;
        }
        System.out.println(res);
    }
    
    public static void dfs(int start, int end, int distance, int cnt, int n) {
        if (end == n - 1) {
            if (cnt == n - 1) {
                res = Math.min(distance, res);
                System.out.println(res);
            }
            return;
        }
        for (int i = 1; i < n; i++) {
            if (!mark[i]) {
                mark[i] = true;
                dfs(end, i, distance + d[end][i], cnt + 1, n);
                mark[i] = false;
            }
        }
    }
}

但是我們可以使用dfs解決時間複雜度爲 n!,題目 n <= 20, 20!= 2432902008176640000 這無疑是非常大的一個數。時間肯定是不允許的。因此我們需要使用更好的辦法。

這題正解其實是利用狀壓DP的方法來做,狀態轉移方程爲

dp[i][j] = min{dp[i][j], dp[i - (1 << j)][k] + map[k][j]}

其中map數組爲權值,map[k][j]是點k到點j的權值

dp[i][j]表示當前已經走過點的集合爲i,移動到j。所以這個狀態轉移方程就是找一箇中間點k,將已經走過點的集合i中去除掉j(表示j不在經過的點的集合中),然後再加上從k到j的權值

問題在於如何表達已經走過點的集合i,其實很簡單,假如走過0,1,4這三個點,我們用二進制10011就可以表示,2,3沒走過所以是0

那麼走過點的集合i中去除掉點j也很容易表示i - (1 << j),比方說i是{0,1,4},j是1,那麼 i = 10011,(1 << j) = 10,i - (1 << j) = 10001

那麼問題的答案就應該是dp[01…111][n-1],表示0~n-1都走過,且當前移動到n-1這個點

分析一下時間複雜度,n爲20的時候,外層循環(1<<20),內層循環20,所以整體時間複雜度O(20∗220)
,這比O(n!)
快多了

import java.util.Arrays;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[][] weight = new int[n][n];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                weight[i][j] = sc.nextInt();
            }
        }
        //m爲所有狀態的二進制表示,如n=2時,m=100,可表示000 001 010 011四種狀態
        //即 都沒經過 經過0號點 經過1號點 經過0號店和1號點四種情況
        int m = 1 << n;
        int dp[][] = new int[m][n];//第一維用下標表示所有狀態,第二維下標表示點號,值表示目前總權
        for(int i=0;i<m;i++) {//初始化所有權爲+∞
            Arrays.fill(dp[i], Integer.MAX_VALUE/4);//除小一點的數都可以,只是爲了防止相加時溢出
        }
        dp[1][0]=0;//意味在出發點0號點,狀態爲000001(即只經歷過0號點),權爲0
        for (int i = 0; i < m; i++) {//遍歷所有狀態
            for (int j = 0; j < n; j++) {//遍歷所有點
                if (((i >> j) & 1) == 1) {//如果這種狀態下經歷過j號點
                    for (int k = 0; k < n; k++) {//搜索從k號點到j號點的最小權
                        if ((((i - (1 << j)) >> k) & 1) == 1) {
                            //如果該狀態經歷過k點但沒有經歷過j點,即是可行狀態
                            //取轉移中的最小值並記錄
                            dp[i][j] = Math.min(dp[i][j], dp[i - (1 << j)][k] + weight[k][j]);
                        }
                    }
                }
            }
        }
        //則dp[m-1][n-1],即經歷過所有點且終點在n-1號點時的權就是結果
        System.out.println(dp[m-1][n-1]);
    }

}

原碼、反碼、補碼

按位取反

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章