起床困难综合症

GitHub跳转原题关系图返回列表

n 扇门都是逐位运算,位与位互不影响:用 0 和全 1 各穿越一次求出每位输入 0/1 的结果,再从高位到低位贪心,免费位不花预算、其余位在不超过 m 时买下。

OJ: roj

题目 ID: 3002

难度:提高

标签:位运算贪心python

创建: 2026-10-01 09:19

更新: 2026-10-07 13:50

形式化题目

给定 nn 扇防御门,每扇门是一个运算 opi∈{AND,OR,XOR}op_i \in \{\text{AND}, \text{OR}, \text{XOR}\} 与一个非负整数参数 tit_i。 定义 f(x)f(x) 为把 xx 依次送过所有门的结果:

f(x)=(⋯((x op1 t1) op2 t2)⋯) opn tn f(x) = \big(\cdots((x\ op_1\ t_1)\ op_2\ t_2)\cdots\big)\ op_n\ t_n

求 max⁡0⩽x⩽mf(x)\max\limits_{0 \leqslant x \leqslant m} f(x)。只需输出最大值,不需要输出取到最大值的 xx。 按位运算的规则是:两个数前补 0 到等长后逐位计算。

样例 n=3, m=10n=3,\ m=10,门依次为 AND 5、OR 6、XOR 7:

xx x∧5x \wedge 5  ∨6\ \vee 6 ⊕7=f(x)\oplus 7 = f(x)
0 0 6 1
1 1 7 0
2 0 6 1
3 1 7 0
4 4 6 1
⋮\vdots
10 0 6 1

为什么偶数得 1、奇数得 0?只看最低位:5=10125 = 101_2 和 6=11026 = 110_2 的第 0 位分别是 1 和 0, 所以 AND 5、OR 6 都把 xx 的第 0 位原样透传;而 7=11127 = 111_2 的第 0 位是 1, XOR 7 把它翻一次:偶数 xx 的第 0 位是 0,翻成 1;奇数 xx 的第 0 位是 1,翻成 0。 更高位更简单:AND 5 把第 2 位以上全部清零,OR 6 再把第 1、2 位固定成 11211_2, 此时值只能是 6 或 7,再经 XOR 7 后全部归零。 所以最终伤害只能取 0 或 1,最大值是 11。 这个样例还提示了两件事:结果既不随 xx 单调增长,也不总在 x=mx = m 处取到 (后者例如 n=1n=1、门 AND 3、m=4m=4:f(4)=0f(4) = 0,而 f(3)=3f(3) = 3 更大),必须真的去做最大化。

正解

思路

朴素做法:枚举 x=0,1,…,mx = 0, 1, \dots, m,每个 xx 依次穿过 nn 扇门,取最大值。 时间是 O(nm)O(nm):最坏 n=105n = 10^5、m=6.6×108m = 6.6 \times 10^8,约 101310^{13} 次运算,不可行。

瓶颈一:候选太多。 要枚举 m+1m+1 个 xx,这本身就是天文数字。能不能不枚举 xx?

关键观察:位与位互不影响。 题面规定两个数前补 0 到等长再逐位运算, AND / OR / XOR 又都是逐位运算——所以一次运算的第 bb 位只用到两个数的第 bb 位。 nn 扇门串起来后这条性质依旧成立:f(x)f(x) 的第 bb 位只取决于 xx 的第 bb 位,与其他位无关。

于是 ff 可以按位拆开。记 fb(c)f_b(c) 为「输入的第 bb 位是 cc 时,输出的第 bb 位」,则

f(x)=∑bfb(xb)⋅2b f(x) = \sum_b f_b(x_b)\cdot 2^b

BB 位的联合决策被拆成了 BB 个互不相干的二元决策,未知量只剩 fb(0)f_b(0) 与 fb(1)f_b(1)。

先建立一点直觉。一扇门对某一比特只有四种可能的作用,写成 (输入 0 的输出, 输入 1 的输出)(\text{输入 }0\text{ 的输出},\ \text{输入 }1\text{ 的输出}):

单扇门 该位输入 0 的输出 该位输入 1 的输出 作用
AND 0 0 0 强制清零
AND 1 0 1 原样透传
OR 0 0 1 原样透传
OR 1 1 1 强制置一
XOR 0 0 1 原样透传
XOR 1 1 0 该位取反

六种门按参数的一位展开就是这四种:tt 的该位是 0 时 AND 是恒 0、OR 与 XOR 是透传; 该位是 1 时 AND 是透传、OR 是恒 1、XOR 是取反。多扇门串起来只是这四种作用的复合, 复合结果仍落在这四种里。所以 fbf_b 只需两个比特就能描述:(fb(0),fb(1))∈{00,01,10,11}(f_b(0), f_b(1)) \in \{00, 01, 10, 11\}—— 00 是恒 0,11 是恒 1,01 是透传,10 是取反。

瓶颈二:求 fb(0)f_b(0)、fb(1)f_b(1) 还要不要 O(nB)O(nB)? 不用。由位独立性, fb(1)f_b(1) 恰好是 f(2b)f(2^b) 的第 bb 位。把所有需要用到的位同时摆成 1,一次穿越就同时得到全部 fb(1)f_b(1):

lo=f(0),hi=f(2B−1) lo = f(0), \qquad hi = f(2^B - 1)

lolo 的第 bb 位是 fb(0)f_b(0),hihi 的第 bb 位是 fb(1)f_b(1)。总共只穿越两次。

瓶颈三:xbx_b 怎么定? 现在目标变成最大化

f(x)=∑bcb(xb)⋅2b,cb(0)=lob,cb(1)=hib f(x) = \sum_{b} c_b(x_b)\cdot 2^b, \qquad c_b(0) = lo_b,\quad c_b(1) = hi_b

这是加权最大化,高位权重大于所有低位之和(2b>∑j<b2j2^b > \sum_{j<b} 2^j), 所以必须从高位到低位决定,高位能拿 1 就拿 1。唯一的约束是 ∑bxb2b⩽m\sum_b x_b 2^b \leqslant m, 用一个累加变量 a 记录已经花掉的预算即可。对第 bb 位:

  1. 若 lob=1lo_b = 1:输入 0 就已经得到 1,白拿,令 xb=0x_b = 0,不花一分预算;
  2. 否则若 hib=1hi_b = 1 且 a+2b⩽ma + 2^b \leqslant m:花 2b2^b 买下这一位,a←a+2ba \gets a + 2^b;
  3. 否则这一位拿不到 1(lob=hib=0lo_b = hi_b = 0,或预算不够),令 xb=0x_b = 0。

第 1 条必须排在第 2 条之前,这是全题最容易写错的一步:既然输入 0 就能拿到 1, 再花 2b2^b 去买 1 就是纯浪费,省下的预算还能在更低位买到更多的 1。 跳过这条优先级,答案会变小——例如 n=4n=4、m=24m=24、门为 AND 6、OR 1、OR 27、XOR 0 时 (此时 lo=27lo = 27、hi=31hi = 31),先买高位的版本取 x=24x = 24 得 27,而正确的是 x=4x = 4 得 31。

三个瓶颈各消除一层,算法就定下来了:读入 nn 扇门 →\to 穿越两次得 lo,hi→lo, hi \to 从高位到低位贪心。

代码

Python 版:

python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 09:20
# update_at: 2026-10-01 09:26

import sys
from collections.abc import Callable
from operator import and_, or_, xor

type BitOp = Callable[[int, int], int]  # 一扇门的运算:把攻击力与参数 t 按位合成
type Door = tuple[BitOp, int]           # 一扇门 = 运算 + 参数

OPS: dict[bytes, BitOp] = {b'AND': and_, b'OR': or_, b'XOR': xor}


def fold(doors: list[Door], x: int) -> int:
    """把攻击力 x 依次送过所有防御门,返回最终伤害。"""
    for op, t in doors:
        x = op(x, t)
    return x


def best_attack(lo: int, hi: int, m: int) -> int:
    """在 0..m 中选初始攻击力,返回最大伤害。

    lo = f(0)、hi = f(2^B-1),它们的第 b 位就是第 b 位输入 0 / 输入 1 的输出。
    从高位往低位贪心:输入 0 就有 1 的位白拿,否则预算够且买得到 1 才买。
    """
    ans = a = 0  # ans = 最终伤害;a = 已花掉的预算(构造中的初始攻击力,低位暂为 0)
    for bit in range(max(lo, hi, m).bit_length() - 1, -1, -1):
        free = lo >> bit & 1                    # 输入 0 就已经是 1:白拿,不花预算
        affordable = a + (1 << bit) <= m        # 这一位还买得起吗(高位已定,低位尚为 0)
        buy = not free and affordable and hi >> bit & 1  # 花钱能买到 1 才花,否则放弃
        ans |= (free or buy) << bit             # free / buy 都是真值,位移后就是这一位的值
        a += buy << bit                         # 只有真花钱时才抬高初始攻击力
    return ans


def solve() -> None:
    data = iter(sys.stdin.buffer.read().split())
    n, m = int(next(data)), int(next(data))
    doors: list[Door] = [(OPS[next(data)], int(next(data))) for _ in range(n)]

    # 每扇门都是逐位运算,某一位的输出只取决于输入的同一位。于是把 x 的所有位一起摆 1,
    # 一次穿越就同时拿到「每位输入 1 的结果」hi;输入 0 的 lo 同理,两次穿越即可。
    top = max(m, max((t for _op, t in doors), default=0)).bit_length()  # 需要决定的位数
    lo, hi = fold(doors, 0), fold(doors, (1 << top) - 1)

    print(best_attack(lo, hi, m))


if __name__ == "__main__":
    solve()

C++ 版(同一算法):

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-06 13:35
 * update_at: 2026-10-06 13:35
 */
#include <iostream>
#include <cstring>
using namespace std;

typedef long long ll;

const int MAXN = 100005; // n 最大约 1e5
const int OP_AND = 0;    // 门运算的编号
const int OP_OR = 1;
const int OP_XOR = 2;

int n;        // 防御门数量
ll m;         // 初始攻击力的上限
int door_op[MAXN]; // door_op[i]:第 i 扇门的运算编号
ll door_t[MAXN];   // door_t[i]:第 i 扇门的参数 t

// 把攻击力 x 依次送过所有 n 扇门,返回最终伤害。
ll apply_doors(ll x) {
    for (int i = 0; i < n; i++) {
        if (door_op[i] == OP_AND) {
            x = x & door_t[i];
        } else if (door_op[i] == OP_OR) {
            x = x | door_t[i];
        } else {
            x = x ^ door_t[i];
        }
    }
    return x;
}

void read_input() {
    cin >> n >> m;
    for (int i = 0; i < n; i++) {
        char op[8];
        cin >> op >> door_t[i];
        if (strcmp(op, "AND") == 0) {
            door_op[i] = OP_AND;
        } else if (strcmp(op, "OR") == 0) {
            door_op[i] = OP_OR;
        } else {
            door_op[i] = OP_XOR;
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);

    read_input();

    // 每扇门都是逐位运算,某一位的输出只取决于输入的同一位。把所有需要决定的位一起摆 1,
    // 一次穿越就同时得到「每位输入 1 的结果」hi;输入 0 的 lo 同理,两次穿越即可。
    ll max_val = m;
    for (int i = 0; i < n; i++) {
        if (door_t[i] > max_val) {
            max_val = door_t[i];
        }
    }
    int top = 0; // 需要决定的位数
    while ((1LL << top) <= max_val) {
        top++;
    }

    ll lo = apply_doors(0);                       // 第 b 位 = 该位输入 0 时的输出
    ll hi = apply_doors((1LL << top) - 1);        // 第 b 位 = 该位输入 1 时的输出

    // 从高位到低位贪心:输入 0 就是 1 的位白拿;否则预算够且花钱能买到 1 才买。
    ll ans = 0;  // 最终最大伤害
    ll used = 0; // 已花掉的预算(即构造中的初始攻击力)
    for (int b = top - 1; b >= 0; b--) {
        ll w = 1LL << b;
        int lo_bit = (lo >> b) & 1;
        int hi_bit = (hi >> b) & 1;
        if (lo_bit == 1) {
            ans |= w;
        } else if (hi_bit == 1 && used + w <= m) {
            ans |= w;
            used += w;
        }
    }

    cout << ans << "\n";
    return 0;
}

复杂度

阶段 代价
读入 nn 扇门 O(n)O(n)
两次穿越求 lo,hilo, hi O(n)O(n)
逐位贪心 O(log⁡max⁡(m,max⁡iti))⩽31O(\log \max(m, \max_i t_i)) \leqslant 31
合计 O(n+log⁡max⁡(m,max⁡iti))O(n + \log \max(m, \max_i t_i))

空间 O(n)O(n),用于存 nn 扇门。最大数据(n=92647n = 92647、m=665522854m = 665522854)实测约 0.04 s、峰值 36 MB, 远低于 1000 ms / 128 MB 的限制。

总结

本题的核心只有两句话:

  1. 按位独立:AND / OR / XOR 逐位运算,所以 ff 的每一位只由 xx 的同一位决定, m+1m+1 个候选取值根本不必枚举;把所有位一起摆 1(再摆 0)穿越两次,就能一次性测出全部位的性质。
  2. 高位优先贪心:位权 2b2^b 大于所有低位权重之和,所以从高位往低位能拿就拿, 但要先用累计预算 a 检查 a+2b⩽ma + 2^b \leqslant m;输入 0 就能拿到的位免费,绝不为它花钱。

这是一个「O(nm)O(nm) 枚举 →\to O(n+log⁡)O(n + \log) 贪心」的典型转化:先用量词结构(按位独立)砍掉候选集合, 再用权重结构(二进制位权)砍掉搜索顺序。

图示解析

下面这张表把样例 n=3,m=10n=3, m=10(门 AND 5、OR 6、XOR 7)的贪心决策逐步摊开, 展示 lolo、hihi 与每一步的预算变化。

B=max⁡(10,7)B = \max(10, 7) 的位长 =4= 4,所以处理第 3 位到第 0 位。两次穿越得到 lo=f(0)=1=00012lo = f(0) = 1 = 0001_2(各位输入 0 的结果),hi=f(15)=0=00002hi = f(15) = 0 = 0000_2(各位输入 1 的结果):

位 bb 2b2^b lolo 的该位 hihi 的该位 预算 aa 判定 xbx_b 伤害位
3 8 0 0 0 lob=hib=0lo_b = hi_b = 0,两种输入都得 0,放弃 0 0
2 4 0 0 0 同上 0 0
1 2 0 0 0 同上 0 0
0 1 1 0 0 输入 0 就已经是 1,白拿 0 1

贪心构造出的 x=0x = 0,伤害 f(0)=1f(0) = 1,与枚举得到的最大值一致。 观察重点有两个:一是四位里只有第 0 位有机会,其余位在 lolo 和 hihi 中都是 0,说明它们被门强制清零了; 二是第 0 位是「白拿」的——输入 0 就有 1,因此不需要动预算,这也说明了为什么 free 必须优先于 buy。