位运算常见操作和农夫过河问题(C++实现)

点点圈 提交于 2020-01-13 17:11:37

二进制常见操作

二进制数中 1 的个数

解法 1

n & 1 判断最右边一位是否为 1,右移之后继续判断最右边一位,重复直到 n = 0。时间复杂度为 O(log2n)

#include <iostream>
using namespace std;
int main() {
    int n = 0b1101, cnt = 0;
    while (n) {
        cnt += n & 1;
        n >>= 1;
    }
    cout << cnt << endl;
    return 0;
}

解法 2

n & (n - 1) 可以消去最右边的 1,重复直到 n = 0。时间复杂度为 O(count),取决于 1 的个数 count,所以这种方法优于第一种。

#include <iostream>
using namespace std;
int main() {
    int n = 0b1101, cnt = 0;
    while (n) {
        n = n & (n - 1);
        ++cnt;
    }
    cout << cnt << endl;
    return 0;
}

判断二进制数某一位为 1 还是 0

判断从右数起第 k 位,(n >> (k - 1)) & 1

#include <iostream>
using namespace std;
int main() {
    int n = 0b1101;
    cout << ((n >> 1) & 1) << endl;  // 第 2 位为 0
    cout << ((n >> 2) & 1) << endl;  // 第 3 位为 1
    return 0;
}

反转二进制数某一位

反转从右数起第 k 位,n ^ (1 << (k - 1))

#include <iostream>
using namespace std;
int main() {
    int n = 0b1101;
    cout << (n ^ (1 << 2)) << endl;  // 反转第 3 位 -> 1001
    cout << (n ^ (1 << 1)) << endl;  // 反转第 2 位 -> 1111
    return 0;
}

进制操作的一些实用规律

一个数按位异或同一个数两次(n ^ x ^ x),值不变。

#include <iostream>
using namespace std;
int main() {
    int n = 13, x = 666;
    cout << (n ^ x ^ x) << endl;  // 仍然是 13
    return 0;
}

用按位与代替模,判断奇偶性,效率更高。n & 1 的结果为 0 就是偶数,结果为 1 就是奇数。

#include <iostream>
using namespace std;
int main() {
    cout << (4 & 1) << endl;
    cout << (5 & 1) << endl;
    return 0;
}

左移 k 位相当于乘 2 的 k 次幂,右移 k 位相当于除以 2 的 k 次幂。在分治策略中很常见,例如归并排序:

#include <cstdlib>
#include <ctime>
#include <iostream>
using namespace std;

void partition(int a[], int l, int m, int r) {
    int *t = new int[r - l + 1];
    int i = l, j = m + 1, k = 0;
    while (i <= m && j <= r) t[k++] = a[i] < a[j] ? a[i++] : a[j++];
    while (i <= m) t[k++] = a[i++];
    while (j <= r) t[k++] = a[j++];
    for (int i = l; i <= r; ++i) a[i] = t[i - l];
    delete[] t;
}

void merge_sort(int a[], int l, int r) {
    if (l < r) {
        int m = (l + r) >> 1;  // 相当于 (l + r) / 2
        merge_sort(a, l, m);
        merge_sort(a, m + 1, r);
        partition(a, l, m, r);
    }
}

int main() {
    srand(time(0));
    int a[20];
    for (int i = 0; i < 20; ++i) a[i] = rand() % 100;
    merge_sort(a, 0, 19);
    for (int i = 0; i < 20; ++i) cout << a[i] << " ";
    return 0;
}

综合例题 —— 农夫过河问题

如果彻底理解了上面的二进制常见操作,再理解这题应该不难。

问题描述:

一个农夫带着一只狼,一只羊和一棵白菜,在河的一侧。农夫需要把所有东西运到河的对岸,但是一次最多只能带一个,而且,当农夫不在时,狼会吃羊,羊会吃白菜。所以,农夫不能让狼和羊独自呆在一起,也不能让羊和白菜独自待在一起,求出农夫能成功过河的方案。

解题思路:

这题可以用很巧妙的二进制操作模拟出农夫过河的过程。0000 表示都没过河,1111 表示都过河,右数第一位表示白菜的状态,第二位表示羊的状态,第三位表示狼的状态,第四位表示农夫的状态。(例如:1010表示农夫带着羊过河,狼和白菜没过河),剩下的就通过深度优先搜索来枚举。

#include <bitset>
#include <iostream>
#include <stack>
#include <vector>
using namespace std;

// 为了方便理解,定义了下列变量
int START   = 0b0000;  // 初始状态
int END     = 0b1111;  // 结束状态
int FARMER  = 0b1000;  // 农夫
int WOLF    = 0b0100;  // 狼
int SHEEP   = 0b0010;  // 羊
int CABBAGE = 0b0001;  // 白菜

vector<int> pre(END + 1, -1);  // 记录前驱,-1 表示没有访问过这个状态
stack<int> temp;  // 用栈实现逆序

bool pos(int state, int k) { return state & k; }  // 判断是否在对岸

bool safe(int s) {
    if (pos(s, FARMER) != pos(s, SHEEP) &&
        ((pos(s, WOLF) == pos(s, SHEEP)) || (pos(s, SHEEP) == pos(s, CABBAGE))))
        return false;
    return true;
}

void dfs(int state) {
    if (state == END) {
        while (state != -2) {
            temp.push(state);
            state = pre[state];
        }
        while (!temp.empty()) {
            bitset<4> bit(temp.top());
            temp.pop();
            cout << bit << endl;
        }
        cout << endl;
    } else {
        for (int k = FARMER; k >= CABBAGE; k >>= 1) {
            int next_state = state ^ (FARMER | k);
            if (pre[next_state] == -1 && pos(state, FARMER) == pos(state, k) && safe(next_state)) {
                pre[next_state] = state;
                dfs(next_state);
                pre[next_state] = -1;  // 回溯
            }
        }
    }
}

int main() {
    pre[START] = -2;  // 标记初始状态,避免出发后又回到初始状态
    dfs(START);
    return 0;
}

最后的结果,一共两种方案:

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!