常見的位運算
與 &
、或 |
、非!
、異或^
左移 >>
、右移<<
補碼
首先我們來簡單介紹一下 原碼、反碼、補碼的概念:
1. 原碼
原碼就是符號位加上真值的絕對值, 即用第一位表示符號, 其餘位表示值. 比如如果是8位二進制:
.
[+1]原 = 0000 0001
[-1]原 = 1000 0001
2. 反碼
反碼的表示方法是:
- 正數的反碼是其本身
- 負數的反碼是在其原碼的基礎上, 符號位不變,其餘各個位取反.
[+1] = [00000001]原 = [00000001]反
[-1] = [10000001]原 = [11111110]反
可見如果一個反碼錶示的是負數, 人腦無法直觀的看出來它的數值. 通常要將其轉換成原碼再計算.
3. 補碼
補碼的表示方法是:
- 正數的補碼就是其本身
- 負數的補碼是在其原碼的基礎上, 符號位不變, 其餘各位取反, 最後+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
例題
求 a 的 b 次方對 p 取模的值。
輸入格式: 三個整數 a,b,p , 在同一行用空格隔開。
輸出格式:輸出一個整數,表示a^b mod p的值。
數據範圍:
首先我們想到用循環暴力法:
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的值。
數據範圍 :
輸入樣例:
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
範圍大約爲 ,long
範圍大約爲,所以直接計算會導致溢出,這就需要我們使用 a + a + a + … + a的方法,因此我們也可以用快速冪思想:
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]);
}
}