H题 | Blackboard

本题解适合刚入门算法的同学,会比较注重细节(啰嗦),已经有算法基础的同学可以直接看官方题解和各位大佬的题解。

解题思路:

首先对于任意两个非负整数 ,如果它们二进制形式的某位不同时为 ,则对于这一位来说, 操作是等价的。

0+0 == 0|0 == 0

0+1 == 0|1 == 1

1+0 == 1|0 == 1

否则, 的结果一定小于 ,因为相对于来说,舍弃了所有的进位,导致结果偏小。

1 | 1 == 01

1+1 == 10

因此我们得出结论:对于连续求和操作 ,将任意位置的 替换为 只会导致结果要么变小,要么不变(不可能导致结果变大)。

为了保证替换前后的公式结果不变,必须满足每个子式 的结果都等于 ,否则将会导致结果不可逆地变小。

题目规定 运算符的优先级大于 ,所以对于任意一种可能的替换方案(不一定合法),我们都可以把 看作分隔符,将数列分割成若干个区间,区间内的数做连续 运算。例如:0 | 1 | 3 + 5 | 7 + 9 可分割成 {0|1|3} + {5|7} + {9}

对于一个连续 运算,当参与运算的任意两个数都满足所有位都不同时为 时,该连续 运算与连续求和操作等价。也就是说,如果某个数的某一个二进制位为 ,那么其他所有的数对应二进制位都必须为

1000 =======非=> 1001
0100 =======法=> 1100
0010 <=合======= 0010
0001 <=法======= 0000

综上,对于任意一种合法的替换方案,形式一定为若干个合法的连续 运算相加。

对于题目给定的连续求和操作 ,这里取一个下标 ,假设我们已经求出了子区间 的所有合法方案数 ,我们只需要把 向后推广到 ,就可以得到本题答案 表示区间 的所有合法方案数,即本题所求)。

这是一种 的思想(动态规划),我们用 表示 ,而现在要做的是求解 (即转移方程)。

我们在区间 后面追加一个 ,这个追加的数导致总方案数发生了变化,可以分成三类讨论:

  1. 存在某些位同时为 这时 必须单独划分到一个连续 运算区间,也就是说 中间的运算符只能是 ,两者绝不能做 运算。因此 的加入不会产生新的方案数)。

  2. 的所有位都不同时为 ,但是存在一个最大的下标 ,满足 存在某些位同时为 因此在区间 内的任意两个数, 操作是等价的,所以 可以并入 所属的连续 运算区间。此时新区间的总合法方案数为:

    看到这一坨公式别害怕,我给你理一理。在区间 内的数字,两两之间都有一个运算符,且对于任意一种合法的替换方案,我们从右往左,找到第一个 运算符。我们可以确定:这个运算符右边只能出现一个连续的 运算区间,左边则是若干个连续的 运算区间相加的形式。形如:(...)+(x|x|...|x|x) 。此时由于这个 右侧的运算符都是确定的,不确定的运算符只存在于这个 的左侧。换句话说,正因为存在左侧这些不确定的运算符,才导致存在不止一种替换方案。而 表示区间 的所有合法方案数,只要你大于 的部分是一个 加上若干个 ,那么你右侧就不存在不确定符号,也就是只有 种方案。因此,这样的区间的合法方案数是左边方案数乘右边方案数,即 。由于第 个数和第 个数不能存在于同一个连续 运算区间内,因此两个数字中间至少存在 。也正因如此,我们枚举 的所有数字,依次令该数字右侧的符号作为整个多项式最右侧的 ,即可覆盖区间 内的所有方案,不重复、不遗漏。因此上述递推公式,本质是枚举所有可能的最右侧 的位置并求和所有对应方案数。

  3. 的所有位都不同时为 ,且存在下标 ,满足 存在某些位同时为 此时只有两种可能:要么 ,此时 取上一次递推的 值即可;要么 和前面所有出现的数都可以进行 运算,取 即可。

聪明的你又发现了:第 种情况是第 种情况的子情况,相当于 ,遂将 条合并。

聪明的你又又发现了:可以开一个长度为 数组,记录每个二进制位最后一次 是在哪个位置的数字上出现的,可以 求解 ,不赖。

聪明的你又又又发现了:在从 开始往 递推时, 是单调不减的,因此第 条中甚至不需要考虑如何取上一个值,因为当 时, 自然不会被更新,本身就是上一个值了。

然鹅,聪明的你并没有就此罢休,叕叕叕发现了: 这个递推方程,右侧不就是连续值求和嘛,于是你想到了用前缀和来 化区间求和的复杂度。优雅,实在太优雅了。

示例代码:

代码中的递推方程是递推的是 而不是 ,导致一个小问题:

存输入数据的时候以 为第一个元素的下标,但是 数组会往边界左侧访问一位,前缀和数组 又会往 数组的边界左侧访问一位,导致 数组可能越界。我的解法是三目运算符手动判边界。当然,你也可以把sum数组开成 unordered_map<int,int> 的形状(或者你想要的任何形状),瞬间解决所有数组越界问题~欸欸,我可没说建议你这么干哦 :)

const int mod = 998244353;
void solve() {
	int n, L = 0, lst[31] = {0};
	cin >> n;
	vector<int> dp(n + 1), sum(n + 1);
	dp[0] = sum[0] = 1; // init

	for (int i = 1, num; i <= n; ++i) {
		cin >> num;
		int pos = 0;
		while (num) {  // move L & update lst[]
			if (num & 1) L = max(L, lst[pos]), lst[pos] = i;
			num >>= 1, ++pos;
		}

		dp[i] = (sum[i - 1] - (L ? sum[L - 1] : 0) + mod) % mod;  // update dp[]
		sum[i] = (sum[i - 1] + dp[i]) % mod;  // update sum[]
	}
	cout << dp[n] << '\n';
}

蒟蒻一枚,质量不高,请各位大佬多指教~