进制转换与位运算

进制

12 个物品被称为 1 打,12 打被称为 1 罗,12 罗被称为 1 格。请问:

  1. 15 个是几打几个?
  2. 6775 个是几格几罗几打?
  3. 2 打 3 个是多少个?
  4. 1 格 9 罗 8 打 10 个是多少个?

以上的“几”均是不小于 0 且小于 12 的整数

分析
  1. 根据 15÷12=1···3,15 个除以每打是几个,就是几打,剩下的就是几个。因此 15 个是 1 打余 3 个。如果把这两个数字写到一起,写成 \((13)_{12}\),则这种计数方式被称为十二进制,因为逢 12 进一位。在这种计数方式下,\((13)_{12}\) 代表 1 打 3 个,等于十进制的 15
  2. 根据 6775÷12=564···7,说明 6775 个等于 564 打余 7 个;564÷12=47···0,说明 564 打等于 47 罗余 0 打;47÷12=3···11,说明 47 罗等于 3 格余 11 罗。因此 6775 个等于 3 格 11 罗 0 打 7 个,若写成 \((31107)_{12}\),则会产生歧义,因为 11 占了两位,而最好每个余数都能表示成一位数。可以另字母 A 为 10,令 B 为 11,这样就可以表示成 \((3B07)_{12}\),因此在十二进制下的 3B07 等于十进制的 6775
  3. 显然是 \(2 \times 12 + 3 = 27\),这说明 \((23)_{12}=(27)_{10}\)
  4. \(1 \times 12^3 + 9 \times 12^2 + 8 \times 12^1 + 10 \times 12^0 = 3130\),这说明 \((198A)_{12}=(3130)_{10}\)

在十六进制下的 ABCD 等于十进制的多少?

分析

十六进制下的一位中,A 代表 10,B 代表 11,C 代表 12,D 代表 13,E 代表 14,F 代表 15,那么 ABCD 转换为十进制就是:\(10 \times 16^3 + 11 \times 16^2 + 12 \times 16^1 + 13 \times 16^0 = 43981\)

任何除了 0 之外的自然数的 0 次方都是 1,对于十六进制来说,每个“个位数”都代表 1 个,每个“十位数”权重是 16,每个“百位数”权重是 \(16^2\),每个“千位数”权重是 \(16^3\),所以可以写成 \((ABCD)_{16} = (43981)_{10}\)

对比十进制很好理解,在十进制下,个位数代表 1 个,十位数权重是 10,百位数权重是 \(10^2\),千位数权重是 \(10^3\),万位数权重是 \(10^4\),在十进制下,可以得到一个显然的结论:\(43981 = 4 \times 10^4 + 3 \times 10^3 + 9 \times 10^2 + 8 \times 10^1 + 1 \times 10^0\)

image

不同进制下同样的数量表示出来的数字看起来差别很大,但是它们表示的是相同的数量,不同进制下“基底”不同——十六进制的基底是 16,而十进制的基底是 10

十进制下的 114514 在十六进制中表示为什么?

分析

每次除以 16 然后取余数,记录所有得到的余数

image

将原来的十进制数字每次除以基底(16),然后分别记录下商和余数,然后继续将商除以 16,以此反复,直到商为 0 为止。从下往上记录每一个得到的余数,就是对应的十六进制数。即 \((114514)_{10} = (1BF52)_{16}\),注意两位数的余数在十六进制下要用字母表示

二进制和其他进制的原理并没有什么不同,但是二进制的特殊之处在于能使用最少的符号数量(0 和 1)表示出所有的整数

二进制下的 10101101 是十进制的多少?

分析

二进制数 10101101 转换为十进制数,最右边一位代表 1,右边第二位代表 2,第三位代表 \(4=2^2\)……最左边一位(其实是从右边数的第 8 位)代表 \(128=2^7\),因此将各位代表的数字加起来,答案就是 \(2^0+2^2+2^3+2^5+2^7=173\)

十进制下的 89 在二进制下如何表示?

分析

image

由于二进制中只使用 0 和 1 两种符号,非常适合使用电子方式实现运算过程

image

例题:P1143 进制转换

分析:可以先将输入的 n 进制数转换为十进制数,然后再将这个十进制数转换为 m 进制数

参考代码
#include <cstdio>
#include <cstring>
const int N = 40;
char num[N];
int ans[N];
int char_to_int(char ch) { // 单个数位符号转成数字
    return ch >= '0' && ch <= '9' ? ch - '0' : ch - 'A' + 10;
}
char int_to_char(int x) { // 数字转成单个数位符号
    return x < 10 ? '0' + x : x - 10 + 'A';
}
int main()
{
    int n, m;
    scanf("%d%s%d", &n, num, &m);
    int dec = 0, len = strlen(num);
    // 原数转换为十进制
    for (int i = 0; i < len; i++) dec = dec * n + char_to_int(num[i]);
    len = 0;
    // 转换为m进制
    while (dec != 0) {
        ans[len] = dec % m; dec /= m; len++;
    }
    // 输出转换好的数字
    for (int i = len - 1; i >= 0; i--) printf("%c", int_to_char(ans[i]));
    printf("\n");
    return 0;
}

本程序中定义了两个函数可以将 char 类型的一位字符转换为 int(例如 '5' 变成 5,'C' 变成 12),也可以将一个 int 类型的数字转换为 char(例如 8 变成 '8',15 变成 'F')

转换为十进制时,不需要每次计算 n 的幂,可以使用迭代的方式(秦九韶算法)提升效率

转换为 m 进制时,由于最先计算得到的余数是最低位,然后是次低位……所以要将这些余数存入数组中,全部计算完毕后反着输出对应的字符

补充:一位十六进制数码对应 4 位数的二进制数码,所以将十六进制和二进制之间相互转换时可以不用十进制为中间跳板,直接进行翻译即可(二进制需要四位四位分组,必须从右向左分组)。例如,二进制数 1010110111 经过分组可以变为 0010 1011 0111,直接口算得到 2B7,反之亦然

选择题:C++ 语言中,以 0b 开头的数是什么进制的数?

A. 二进制
B. 八进制
C. 十进制
D. 十六进制

答案

A。0b 开头的是二进制,0o 开头的是八进制,0x 开头的是十六进制。

完善程序题:

优美的进制。问题:给出整数 \(n\),认为 \(k\) 进制是优美的,当且仅当 \(n\)\(k\) 进制下至少有两位,且每一位的数值都不同。求对于给定的 \(n\),有哪些进制是优美的,不存在则输出 \(-1\)。试补全程序。

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100000;
int n;
int vis[MAXN], a[MAXN];
vector<int> ans;
int check(int k) {
    int x = n, top = 0;
    for (int i = 0; i <= k; i++) vis[i] = 0;
    while (①) {
        a[++top] = ②;
        x = ③;
    }
    if (top < 2) 
        return 0;
    for (int i = 1; i <= top; i++) {
        if (④)
            return 0;
        vis[a[i]] = 1;
    }
    return 1;
}
int main() {
    cin >> n;
    for (int i = ⑤; i <= n; i++) {
        if (check(i))
            ans.push_back(i);
    }
    if (ans.empty()) {
        cout << -1;
    }
    for (int i = 0; i < ans.size(); i++)
        cout << ans[i] << " ";
    return 0;
}

①处应填?
A. x>0 / B. x>1 / C. x/k>0 / D. x/k>1

②处应填?
A. x/k / B. x%k / C. (x-1)/k+1 / D. (x-1)%k+1

③处应填?
A. x/k / B. x%k / C. (x-1)/k+1 / D. (x-1)%k+1

④处应填?
A. vis[i]==1 / B. vis[a[i]]==0 / C. vis[i]==0 / D. vis[a[i]]==1

⑤处应填?
A. 1 / B. n-1 / C. 2 / D. 0

答案

程序的整体思路是枚举每一种进制,将 \(n\) 转换成对应进制下的结果,并检查“是否每一位数值都不同”,将符合条件的进制记录下来,最终输出。

image

答案:ABADC。

二进制与数据存储

计算机内存只能一位一位地存储 0 和 1,那么内存中如何存储各种数据类型?
假设在 C++ 中定义了这些变量:

int a = 233;
int b = -233;
float c = 3.14;
char d[4] = "Ha!";

image

上图表现了计算机内存中变量的存储方式,“0x”是十六进制数字前的前缀

内存非常大,如果希望定位到某个变量,就需要知道这个变量所在的地址。假如在一台 32 位计算机中,地址是 32 位二进制数,可以缩写为 8 位十六进制。一个 0 或 1 的数码被称为一,8 位被称为一字节,也就是 1B(Byte)

一个 int 类型或 float 类型的变量占用 32 位空间。十进制数字 233 转换为二进制数字为 11101001,所以 233 存储在内存中,在低位(右边)填入 11101001,而左边(高位)用 0 填充。十进制的 -233 是一个负数,在内存中就会表示为 1···100010111,高位是用 1 填充的。而浮点数比较复杂,需要将十进制的浮点数转换为二进制的浮点数,然后在内存中分别记录符号、指数和有效数字

一个 char 类型的变量占用 8 位,大小为 4 的 char 类型数组占用 32 位。将这个数组中的每个原数的 ASCII 码转换为二进制后直接存入内存中。例如,'H' 的 ASCII 码值为 72,'a' 的 ASCII 码值为 97,'!' 的 ASCII 码值为 33;字符串最后还有一个 '\0',对应的值是 0

定义一个变量,就会为这个变量准备一块内存空间,并记录这个空间的起始地址,当访问到这个变量的时候,就会根据地址在内存中找到这个变量的值

有些变量类型也有无符号数,例如 unsigned int 类型,这个类型和 int 类型一样占用 32 位,但是以放弃存储正负符号为代价,可以存储 \(0\)\(2^{32}-1\)

计算机中还有其他表示数据大小的单位,比如 1KB 是 \(2^{10}=1024\) 字节,1MB 是 \(2^{20}\) 字节,1GB 是 \(2^{30}\) 字节

负数转二进制

考虑到 int 占用 32 位太长,这里使用只占 8 位的 signed char 类型举例,57 用二进制表示为 00111001(补足 8 位)。要表示一个负数,那就要占用最高位的一位来表示正负,0 表示非负,1 表示负数

  • 用除了第一位的数字表示这个负数的绝对值,第一位变成 1,这样 -57 表示为 10111001,这种表示方式称为原码。一般计算机不使用这种方式来表示负数

  • 将负数的绝对值对应的数全部取反,由 1 变为 0,由 0 变为 1,这样 -57 表示为 11000110,这种表示方式称为反码。使用反码有一个问题:0 有两种表示方式(全 0 和全 1),所以也不常用

  • 先计算负数的反码,然后加 1,这样 -57 表示为 11000111,这是计算机使用的表示负数的方法,被称为补码。这种表达方式下,0 只有 1 个,全 1 代表 -1

有了补码这种表示负数的方式,计算机就可以很方便地计算二进制减法了。例如要计算 66-57 时,可以认为是 66+(-57)。66 的二进制是 01000010,-57 的二进制是 11000111,列竖式累加

  ..    ..  (进位记号)
  0100 0010
+ 1100 0111
-----------
 10000 1001

由于这个数字溢出了 8 位,所以只取低位数的 8 位,得到的答案是 00001001,也就是十进制下的 9。补码这种非常巧妙的设计使得计算机可以化减为加。但是谈论到补码时必须要确定总位数,例如 8 位下的有符号整数实质上是在 -128~127 之间形成了一个环

使用 memset 给 int 数组初始化时,如果想要精准的初始化,只能初始化为 0 或 -1(而给一个其他数字,则不会将数组初始化为这个数字),因为 memset 只能将一片数组区域的每一个字节初始化为这个数字(小于 255),而一个 int 是由 4 字节组成的,所以只能填充成全 0(最后的值还是 0)或者全 1(最后的值是 -1),所以通常只使用 memset(a, 0, sizeof(a)) 或者 memset(a, -1, sizeof(a)) 这样的写法将整个数组初始化为 0 或 -1。而如果写出 memset(a, 3, sizeof(a)) 这样的代码时,实际上是把每个元素初始化成了 0x03030303

小数转二进制

将实数从十进制转换为二进制,可以将整数部分和实数部分分别处理。如 3.14,整数部分的 3 是二进制的 11;而小数部分 0.14 如下图所示处理

image

将原来的小数数字,每次都乘 2,如果得到的整数部分是 1,则答案记录一个 1,并去掉这个整数部分,然后继续运算;如果得到的结果中的整数部分还是 0,那么答案记录一个 0,继续计算。因此 3.14 表示为二进制数是 11.00100011···,在二进制下是一个无限小数。因此,这就是计算机浮点数类型无法精确表示很多实数的原因

那么,如何将一个二进制小数转换成十进制呢?例如 101.101,同样将整数部分和小数部分分开,整数部分十进制是 5,小数部分的计算方式和整数转换方式差不多:\(1 \times 2^{-1} + 0 \times 2^{-2} + 1 \times 2^{-3} = 0.625\),所以整个数的十进制就是 5.625

计算题:十进制数 10.375 转换为八进制数的结果是?

答案

12.3

逻辑命题

在逻辑学中,命题指的是判断一件事情的陈述句,且有明确的真伪。一般用 1 表示真命题,用 0 表示伪命题

多个命题可以进行复合,进行与、或、非、异或等操作

  1. 或:\(A \vee B\),两个命题中至少有一个真命题时,其复合命题为真

  2. 与:\(A \wedge B\),两个命题必须全为真命题,其复合命题才是真命题

  3. 非:\(\lnot A\),将原命题取反

  4. 异或:\(A \oplus B\),两个命题一真一假时复合命题为真,等价于 \((A \wedge \lnot B) \vee (\lnot A \wedge B)\)

有时为了简化逻辑表达式,可以将或运算变成加号,与运算变成乘点(甚至可以省略),而非运算变成上划线。例如,\(\lnot ((A \wedge \lnot B) \vee (\lnot A \wedge B))\) 可以表示为 \(\overline{A \overline{B} + \overline{A} B}\)

之所以能将或运算变成加号、与运算变为乘号,是因为逻辑运算有和普通代数运算有类似的性质,而且与运算的优先级高于或运算

  1. 交换律:AB=BA, A+B=B+A

  2. 结合律:(AB)C=A(BC), (A+B)+C=A+(B+C)

  3. 分配律:\(A(B+C)=AB+AC\)

除此之外,还有一些显然的性质:

  1. A+1=1, 0A=0

  2. \(AA=A, A+A=A, A + \overline{A} = 1\)

还有一个非常重要的德·摩根定律,使得与运算和或运算可以在一定条件下互相转化:

  1. \(\overline{A} + \overline{B} = \overline{AB}, \overline{A} \cdot \overline{B} = \overline{A+B}\)

以上逻辑运算性质可以化简一个复杂的逻辑表达式,便于求出逻辑表达式的值

化简逻辑表达式为最简与或式。由“与运算”连接的一组变量(或者带非运算的变量)叫“与项”,将一些“与项”用“或运算”连接的表达式为与或式。最简与或式是指与项数量最少,同时每项数量也最少的与或式

编程时有时会出现一些比较复杂的条件判断语句,可以使用化简逻辑运算的方法与技巧来化简判断语句,或者明确有些判断语句是等效的。例如 !((x <= 0 || x > 5) && (y <= 0 || y > 10)) 等价于 x > 0 && x <= 5 || y > 0 && y <= 10

image

答案

A

位运算

#include <cstdio>
int main()
{
    int a = 85, b = 51;
    int p = a & b;
    int q = a | b;
    int r = a ^ b;
    int s = ~a;
    int u = a << 2;
    int v = a >> 3;
    printf("%d %d %d %d %d %d\n", p, q, r, s, u, v);
    return 0;
}

运行程序,得到的结果是

17 119 102 -86 340 10

这里涉及到了 C++ 中的位运算,也就是直接对整数在内存中的二进制位进行按位操作

& 运算是按位与,注意只有一个符号,&& 是逻辑与。该符号将前后两个操作数按位对齐,然后每一位上都进行与运算,最后得到位运算的结果。例如,85 的二进制数为 1010101,51 的二进制数为 110011,计算过程是这样的(int 类型是 32 位二进制数):

   a 0000 0000 0000 0000 0000 0000 0101 0101
&  b 0000 0000 0000 0000 0000 0000 0011 0011
--------------------------------------------
   p 0000 0000 0000 0000 0000 0000 0001 0001 

可以发现,每一位都进行了与运算,最后得到的结果是 10001,也就是十进制的 17

| 符号是按位或;^ 符号是按位异或。异或运算符的优先级高于按位或运算,但是低于按位与运算

而 ~ 符号是取反;<< 符号是按位左移;>> 符号是按位右移,它们运行的机理是这样的:

   a: 00000000000000000000000001010101
  ~a: 11111111111111111111111110101010
a<<2: 00000000000000000000000101010100
a>>3: 00000000000000000000000000001010

可见,取反就是将这个数字的二进制数 0 变 1、1 变 0,然后根据前面介绍的补码,就可以知道转换后的数字。对于带符号整数来说,~a 的值和 -a-1 的值是一样的。

而左移是将这个二进制数的所有位数往左移动指定的位数,右边用 0 补齐,左边截掉。而右移则是将这个二进制数的所有位数往右移动指定的位数,右边截掉。右移时,如果原数是非负数,则左边补 0,否则左边补 1,因此 a<<n 等于 a 乘 2 的 n 次方,a>>n 等于 a 整除 2 的 n 次方(负数右移运算是下取整的整除,如 (-3) >> 1 是 -2,可以用于二分时替代 /2,因为负数 /2 是向零取整)。在使用左移和右移运算符时,左移/右移的位数非负且注意值不要溢出。

如果对位运算的优先级不熟悉,建议编程时多打括号。

已知一个正整数变量 a,对这个数的二进制数列进行以下操作,尝试用位运算符号写出操作方式:

1) 将最后一位的右边加上一个 1,例如 101 变为 1011

分析

首先将 101 左移 1 位变为 1010,然后再加上 1,表达式为 (a<<1)+1,注意左移右移的优先级低于加减乘除,但是高于除了取反以外的逻辑运算符(与、或、异或)

2)将最后一位变为 0,例如 1010 或者 1011 处理后都变成 1010

分析

第一种方法是将它和 1 相或,使其最后一位变成 1 后减 1(不能直接和 1 相与,否则除最后一位外都没了),表达式是 (a|1)-1;第二种方法是和 11···110(十进制的 -2)相与,保留左边的所有位数,而最右边变为 0,表达式是 a&-2

3)取末 5 位序列,例如 11011010 处理后得到 11010

分析

可以知道与运算有“割草机”的作用,如果原数和 0 相与,则会被“割掉”(无论原数是 0 还是 1,都会变成 0),否则就保留原数不变。因此可以构造一个右边是 5 个 1 的剃刀(也就是 0···011111),这个数字刚好就是 0···0100000 减去 1 得到的,所以表达式是 a&((1<<5)-1)

选择题:奇偶校验编码是常见的校验编码方式。对于二进制编码 \(A_nA_{n-1} \dots A_2A_1\),奇偶校验编码在编码的最后增加一位校验位 \(G\),并将原编码与校验位作为整体发送。校验位分为奇校验位与偶校验位,奇校验位保证 \(A_n \oplus A_{n-1} \oplus \dots \oplus A_2 \oplus A_1 \oplus G = 1\),偶校验位保证 \(A_n \oplus A_{n-1} \dots \oplus A_2 \oplus A_1 \oplus G = 0\)。下列编码与校验位对应正确的是?

  • A. 编码 \(11100111\),奇校验位 \(0\)
  • B. 编码 \(01100010\),偶校验位 \(0\)
  • C. 编码 \(00010010\),奇校验位 \(1\)
  • D. 编码 \(11100010\),偶校验位 \(1\)
答案

C

常见位运算操作

判断某一位是否为 1:(x >> i) & 1 或者 x & (1 << i)

判断是否有相邻的 1:x & (x >> 1)

x 是 y 的子集:(x & y) == x 或者 (x | y) == y

若 x 是 y 的子集,取 x 以 y 为全集的补集:y ^ x

将某一位置 1:x | (1 << i)

将某一位反转:x ^ (1 << i)

取 x 拥有的最低位的 1(lowbit):x & -x 或者 x & (~x + 1)

阅读程序题:

#include <bits/stdc++.h>
using namespace std;
int x, y;
unsigned int n;
int main() {
	cin >> n >> x >> y;
	unsigned int mask = 0xff;
	int x8 = x << 3;
	int y8 = y << 3;
	unsigned int nx = (n >> x8) & mask, ny = (n >> y8) & mask;
	n &= (~(mask << x8));
	n &= (~(mask << y8));
	n |= (nx << y8);
	n |= (ny << x8);
	cout << "0x";
	cout << std::hex << n << endl;
	return 0;
}

假设输入的 n 是 32 位无符号整数范围内的整数,x,y 是不超过 3 的自然数。

判断题:

代码中 mask 变量的值转化为二进制的低 16 位结果是 0000 0000 1111 1111

答案

正确。0xff 在 unsigned int 下就是前 24 位二进制为 0,后 8 位二进制为 1 的数,因此低 16 位是 0000 0000 1111 1111

当输入 x=0 的时候,nx 表示 n 中最低八位对应的字节的数据。

答案

正确。当 x=0 时,x8=x<<3=0,因此 nx=(n>>x8)&mask 相当于 n 直接与 mask 位与,所以就是取了 n 二进制下的低八位。

去掉程序第 11 行至第 12 行中 (~(mask << x8))(~(mask << y8)) 两处中的最内层括号不会改变程序的结果。

答案

错误。去掉括号后,~mask 会先计算,与原来的计算顺序不同。

单选题:

当输入为 15078 0 1 时,变量 nxny 的值分别为多少?(提示:十进制数 15078 与十六进制数 3AE6 相同)
A. 0xE6, 0x3A / B. 0x6, 0xE0 / C. 0x6, 0xE / D. 0x6, 0xA

答案

A

image

当输入为 23270 0 1 时,输出为?
A. 0x5A6E / B. 0x5E6A / C. 0xA56E / D. 0xE65A

答案

D

image

以下哪一个变量的类型修改可能影响程序的输出?
A. 将 x,y 修改为 unsigned int 类型。
B. 将 x8,y8 修改为 short 类型。
C. 将 mask 修改为 int 类型。
D. 将 nx,ny 修改为 unsigned long long 类型。

答案

C。考点:有符号、无符号整数类型,表达式计算中的隐式类型转换。

A选项:题目保证输入的 x,y 是不超过 3 的自然数,因此只能是 0/1/2/3,而这两个变量的作用是在第 8~9 行进行左移运算(对应结果为 0/8/16/24),而 x8,y8 最终都是 int 类型,因此不管 x,yint 类型还是 unsigned int 类型对 x8,y8 的计算结果没有影响。

B选项:如上分析,x8,y8 需要存的值是 0/8/16/24 中的某个,因此在这两行时 short 类型依然足够。而 10~14 行中用到 x8,y8 时另一侧的运算数 n,nx,ny,mask 都是 unsigned int 类型,所以不管 x8,y8int 类型还是 short 类型在参与运算时都会被隐式转换为 unsigned int 类型,因此对输出没有影响。

C选项:分析第 11~12 行,因为 x8,y8int 类型,所以 mask<<x8mask<<y8 这个表达式中如果 maskint 类型,则整体计算也基于 int 类型,如果 maskunsigned int 类型,则整体计算基于 unsigned int 类型。考虑 x8,y8 取到 24 的场景,mask 原本的值相当于一个最后八位全为 1 的二进制数,这个数左移 24 位如果是 unsigned int 类型,则正好还没有溢出。而如果是 int 类型,左移 24 位之后正好溢出到符号位,触发未定义行为(指 C/C++ 语言标准对此时的程序行为没有明确限定的语义,由编译器自行决定实现效果,编程中应尽可能避免触发未定义行为),因此这里的类型改变可能影响程序的行为。

D选项:首先第 10 行的计算结果显然不会造成影响。而第 13~14 行中,改成 unsigned long long 也不过是让左移 x8y8 的计算结果前面多补几个 0 而已,最终这个结果要和 unsigned int 类型的 n 做按位或的运算,因此那些高位的 0 在最终接收的变量 n 中全被截掉了,对数值大小没有影响。

例:P7076 [CSP-S2020] 动物园

解题思路

对于 a 中的每一个数,看在哪些二进制位上是 1,用 f1 标记;再看 m 条要求里对哪些位有要求,用 f2 标记

求出有多少位是被 f1 标记的或者没被 f2 标记的,若满足这样条件的位数有 x 个,则答案为 \(2^x - n\),结果需要使用 unsigned long long(用 printf 输出时对应的格式化占位符为 %llu

\(2^x\) 在不溢出的情况下可以写成 1 << x,如果需要得到 long long 类型的结果可以写成 1ll << x,要得到 unsigned long long 类型的结果则是 1ull << x

当 x 是 64 时,可以将 \(2^{64}\) 拆成两个 \(2^{63}\),计算 (1ull << 63) - n + (1ull << 63),防止溢出

注意特判当 x 是 64 而 n 是 0 的情况,此时答案为 \(2^{64}\),超出了 unsigned long long 的表示范围,没法用整数类型的变量输出,应直接算出具体数字后以字符串形式输出结果

参考代码
#include <cstdio>
using namespace std;
typedef unsigned long long ULL;
int main()
{
    int n, m, c, k;
    scanf("%d%d%d%d", &n, &m, &c, &k);
    ULL f1 = 0, f2 = 0;
    for (int i = 1; i <= n; i++) {
        ULL x; scanf("%llu", &x);
        f1 |= x;
    }
    for (int i = 1; i <= m; i++) {
        int p, q; scanf("%d%d", &p, &q);
        f2 |= 1ull << p;
    }
    int cnt = 0;
    for (int i = 0; i < k; i++) {
        if (((f1 >> i) & 1) || !((f2 >> i) & 1)) cnt++;
    }
    if (cnt == 64 && n == 0) printf("18446744073709551616\n");
    else if (cnt == 64) printf("%llu\n", (1ull << 63) - n + (1ull << 63));
    else printf("%llu\n", (1ull << cnt) - n);
    return 0;
}
posted @ 2023-12-02 11:50  RonChen  阅读(194)  评论(0编辑  收藏  举报