LuoguP7637 [BalticOI 2006 Day 1] BITWISE EXPRESSIONS

发布时间 2023-08-25 15:53:19作者: Wiueh_Plus

题目大意

给定 \(N\) 对数据,每对数据包含两个整数 \(A_i\)\(B_i\),表示这一对数据的 \(v_i\) 的范围:\(A_i \leq v_i \leq B_i\)。又将这 \(N\) 对数据分为 \(P\) 组,其中 \(K_i\) 表示第 \(i\) 组数据中有多少对数据。

我们设第 \(i\) 组数据中将所有数按位与的结果为 \(X_i\),求 \(X_1\&X_2\&...X_P\) 的最大值。

题目分析

思路:贪心

很容易看出来,我们要想最终的结果最大,那么我们需要尽可能地满足最高位的需求,也就是优先考虑将最高位置为 \(1\)

这个贪心很容易就可以证明出来,因为对于一个二进制数,如果这个二进制数的第 \(x\) 为由 \(0\) 变成了 \(1\),那么它将会增加 \(2^{x-1}\)。相较于两种方案,将当前最高位 \(x\)\(0\) 变成 \(1\),或是将所有低于 \(x\) 位的都变成 \(1\),显然,\(2^{x-1}>\sum_{i=x-2}^02^i\)。说明这个贪心是正确的。

我们为了方便操作,用 \(st\) 数组表示每一组数据的开头的下标。输入:

n = read(),p = read();
st[1] = 1;
for (int i = 1;i <= p;i++){
	int x = read();
	st[i + 1] = st[i] + x;
}
for (int i = 1;i <= n;i++){
	arr[i].l = read(),arr[i].r = read();
}

解决问题时,我们会遇到几个问题。

\(\bullet\) 我们如何知道一个连续的区间内存不存在一个数的二进制上的某一位上为 \(1\)

对于这个问题,我们可以用一些巧妙的位运算将这个过程变成 \(O(1)\) 的复杂度。

对于一个连续的区间 \([l,r]\),我们要判断这连续的区间内存不存在一个数的二进制的第 \(x\) 位上为 \(1\)。首先判断 \(l\) 的第 \(x\) 位上是不是 \(1\),如果是,就说明存在。否则,我们找到第一个大于 \(l\) 的二进制数的第 \(x\) 位上为 \(1\) 的数,用这个数判断与 \(r\) 之间的关系,若小于等于 \(r\),则存在,否则不存在。

根据刚才贪心的证明,我们很容易得到第一个大于 \(l\) 的符合要求的二进制数,即把 \(l\)\(x\) 位赋值为 \(1\),把 \(x\) 位后的所有位置变成 \(0\)。用代码表示为:

(l + (1ll << (bit - 1))) & -(1ll << (bit - 1)

\(\bullet\) 每对数据是一个区间,如果两次取到不同的数,并且两个数的二进制完全不一样,导致了错误怎么办?

我们可以将每对数据已经确定了的位数存入一个数组 \(done\) 里,\(done_{num,i}\) 表示第 \(num\) 对数据中,第 \(i\) 个已经确定出的位置(即已经贡献出了的二进制位),这样我们每次选数的时候,判断一下当前这个数具不具备这一对数据中的条件(即包不包括之前确定了的二进制位)。

这意味着我们可以将不包括之前确定了的二进制位的数直接舍弃吗?答案肯定是不可以。

因为这仅仅代表这对数据中的其中一个数,直接舍弃就太片面了,我们考虑若他不具备之前已经确定的二进制位,我们就强行把这些二进制位加上,看加上之后会不会超出原本数据的范围。即:

int add_bit(int x,int num){ // x 表示第一个大于 l 的符合要求的二进制数,num 表示第 num 对数据。
	for (int i = 1;i <= donei[num];i++){ // donei 用于表示下标。
		if (((x >> (done[num][i] - 1)) & 1) == 0){ // 如果不包括之前已经确定的二进制位。
			x += (1ll << (done[num][i] - 1)); // 强行加上。
		}
	}
	return x;
}

这样我们如果 \(x>r\) 我们还不能着急去舍弃他。因为 \(x\) 的二进制中还有可能存在其他不必要的二进制数,也就是除了必要的已经确定的二进制位以外,还存在 \(x\) 自身本来就有的二进制位,我们尝试把这些二进制位适当的取舍,看看最终能不能满足条件。

这里有个细节,我们枚举位数时应该从大到小,不然如果从小到大的话,越到后面可能多减的也就越多。

bool in(int bit,int num){ // 用来判断第 bit 位是不是第 num 对数据必要的二进制位。
	for (int i = 1;i <= donei[num];i++){ // 枚举。
		if (bit == done[num][i]){ // 是必要的二进制位。
			return 1;
		}
	}
	return 0;
}
bool delcheck(int x,int l,int r,int num,int bit){
	for (int i = 32;i >= 1;i--){ // 根据题目给出的数据范围,二进制下最多 32 位。
		if (!in(i,num) && ((x >> (i - 1)) & 1) && i != bit){ // 如果第 i 为不是必要的二进制位,并且目前第 i 位恰好为 1,并且第 i 位也不是现在要加的二进制位。
			if (x - (1ll << (i - 1)) >= l && x - (1ll << (i - 1)) <= r){ // 如果减去这一位后满足范围,返回真。
				return 1;
			}
			else if (x - (1ll << (i - 1)) > r){ // 如果减去这一位后还是太大了,那么先减去,看后面有没有机会继续减。
				x -= (1ll << (i - 1));
			}
            // 如果减去太小了,就不减这一位,即不作任何操作。
		}
	}
	return 0; // 如果所有位数都枚举完了还不满足条件,返回假。
}

那么此时此刻我们的 \(\operatorname{check}\) 函数就可以写出来了(用来判断当前这对数据能否贡献)。

bool isgreater(int x,int l,int r,int num,int bit){ // 判断是否大于 r
	if (x > r){ // 如果大于 r
		return delcheck(x,l,r,num,bit); // 进行上文所说的删位处理。
	}
	return 1; // 不大于r,返回真。
}
bool check(int l,int r,int bit,int num){
	if ((l >> (bit - 1)) & 1){ // 如果 l 的第 bit 位上本来就是 1。
		int x = add_bit(l,num); // 判断 l 的二进制包不包括之前已经确定的二进制位,没有则加上。
		return isgreater(x,l,r,num,bit); // 判断是否大于 r。
	}
	int x = ((l + (1ll << (bit - 1))) & -(1ll << (bit - 1))); // 上文所描述的第一个大于 l 的满足条件的数。
	x = add_bit(x,num); // 判断 x 的二进制包不包括之前已经确定的二进制位,没有则加上。
	return isgreater(x,l,r,num,bit); // 判断是否大于 r。
}

\(\bullet\) 如果一个组内刚开始全都是一对数据在贡献,其他数据都不做贡献,会对最优解产生影响吗?

当然会,比如前面几位都是一对数据在贡献,这个时候出来一位,这一对数据本来是可以贡献的,但是由于前面几位数的贡献,这对数据容不下这一位了,并且组内的其他数据也无法贡献出这一位(其他数据不是因为容不下,而是区间内根本没有这一位)。但如果其他数据可以帮忙分担一下前面几位,那么就可以给其他数据留下充足的空间来存储别的数据根本没有的位数。

这个时候我们开一个数组 \(times\)\(time_i\) 表示第 i 对数据已经贡献出的次数。我们每次在组内找出能够存储这一位的,并且贡献次数最小的来存储当前这一位即可,这样我们就把数据的利用价值最大化了。

int ans = 0; // 统计答案
for (int bit = 32;bit >= 1;bit--){ // 枚举位数。
	int cnt = 0; // 统计能够贡献出当前位的组数。
	for (int idx = 1;idx <= p;idx++){ // 枚举组。
		int minn = 2147483647,mini; // 查找贡献最小的。
		for (int i = st[idx];i <= st[idx + 1] - 1;i++){ // 枚举组内每对数据。
			if (check(arr[i].l,arr[i].r,bit,i)){ // 判断当前数据能否贡献。
				if (minn >= times[i]){ // 找已经贡献的最小的数据。
					minn = times[i];
					mini = i;
				}
			}
		}
		if (minn != 2147483647){ // 如果有能贡献的。
			cnt++; // 统计组数。
			usei[cnt] = mini; // 统计这一组的贡献者。
			times[mini]++; // 将这一组的贡献者的贡献次数 +1。
		}
	}
	if (cnt == p){ // 如果贡献的组数等于总组数,说明这一位可以变成 1。
		for (int i = 1;i <= p;i++){ // 将当前已经确认的位数,统计到 done 数组里面。
			donei[usei[i]]++;
			done[usei[i]][donei[usei[i]]] = bit;
		}
		ans += (1 << (bit - 1)); // 统计答案。
	}
}
printf("%lld",ans);

那么这样一来这道题就已经完成了。总的代码就不放了,把上文的几段合起来就行了。

总结

这道题有一点思维难度,还要求了对于位运算的掌握。特别是对细节的处理很重要,经常忘记想到一些其他的情况而导致出错。