【ICPC】2020上海站 C. Sum of Log | 动态规划、二进制、分类讨论

73 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第19天,点击查看活动详情

【ICPC】2020上海站 C. Sum of Log | 动态规划、二进制、分类讨论

题目链接

Problem - C - Codeforces

题目

image.png

题目大意

对于给定的正整数 X,YX,Y,范围是 0X,Y1090\le X,Y\le 10^9,要求输出

i=0Xj=[i=0]Y[i&j=0]log2(i+j)+1\sum_{i=0}^X\sum_{j=[i=0]}^Y {[i\&j=0]\lfloor log_2(i+j)+1\rfloor}

其中:

  • &\& 表示位运算中的“与”运算。
  • 中括号 [][] 的含义为:当表达式 AA 的值为真时,[A]=1[A]=1,否则 [A]=0[A]=0
  • x\lfloor x\rfloor 表示不大于 xx 的最大整数。

思路

熬了大通宵早八摸鱼半梦半醒写的 DP,提交一发AC……睡了一下午起来乍一看根本不知道自己在干什么(x
让窝比着代码胡乱分析(x

首先观察题目给出来的式子:

i=0Xj=[i=0]Y[i&j=0]log2(i+j)+1\sum_{i=0}^X\sum_{j=[i=0]}^Y {[i\&j=0]\lfloor log_2(i+j)+1\rfloor}

容易发现当 (i&j)0(i\&j)\neq 0[i&j=0]=0[i\&j=0]=0,对答案没有贡献。

我们只需要考虑 iijj 没有相同的位置是 11 的情况,在此情况下 i+ji+j 不会进位,所以 log2(i+j)+1\lfloor log_2(i+j)+1\rfloor 其实等价于 log2(max(i,j))+1\lfloor log_2(max(i,j))+1\rfloor。也就相当于把我们枚举的 iijj 转化为二进制形式,取较长的长度。

我们枚举所有 iijj 所有可能的二进制下的长度,对于每一个可能的长度 lglg,我们使用 DP 来求解方案数。

我们选择的数字 iijj 分别不能超过 XXYY。在二进制形式下,自高位向低位试图选择 ii 时:

  • 当我们枚举到了第 tt 位,如果存在更高位 XX11ii 填了 00,说明第 tt 位可以乱选 0101,都不会大于 XX
  • 如果不存在上述情况,说明我们从最高位到第 t+1t+1 位,每个位置上的数字都与 XX 相同,所以 ii 的第 tt 位选的数字不能超过 XX 的第 tt 位。

所以我们将 DP 状态设置为从最高位位到第 tt 位选择的方案数,且在后续的转移中:

  • dp[t][0][0]dp[t][0][0]iijj 均不能乱选。
  • dp[t][0][1]dp[t][0][1]ii 不能乱选,jj 可以乱选。
  • dp[t][1][0]dp[t][1][0]ii 可以乱选,jj 不能乱选。
  • dp[t][1][1]dp[t][1][1]iijj 均可以乱选。

转移如下图所示:

image.png

不管怎么组织还是乱糟糟的……于是将 DP 细节的讲解以代码注释的形式给出 QAQ

代码

#include <stdio.h>
#include <algorithm>
#include <string.h>
using namespace std;
using LL=long long;
const LL mod=1e9+7;
const int N=31;
LL dp[N][2][2];
int X,Y;
LL solve()
{
	LL ans=0;
	scanf("%d%d",&X,&Y);
	for (int i,lg=29;lg>=0;--lg)
	{
                // 枚举到 log_2(i+j)+1 下取整是 lg
                // 如果 X 和 Y 都没有 lg 那么长,直接跳过
		if (X<(1<<lg)&&Y<(1<<lg)) continue;
		memset(dp,0,sizeof(dp));
                
                //首先我们进行初始化DP数组
                
                /*
                    如果 X 比两倍的 2 的 lg 次方大,说明 X 的更高位存在 1,
                    但是我们枚举的 i 长度不超过 lg,
                    所以后续转移中 i 的每一位都可以乱选。
                    Y 同理。
                */
		if (X>=(1<<lg+1)&&Y>=(1<<lg+1)) dp[lg][1][1]=2;
		else if (X>=(1<<lg+1))
		{
                        /* 
                            现在是 i 都可以乱选的情况,且第 lg 位必须有一个 1。
                            不管 Y 的第 lg 位是什么,
                            都可以让 j 选择 Y 的第 lg 位,相应填充 i 的第 lg 位。
                            如果 Y 的第 lg 位是 1,
                            我们可以让 j 填 0,那么 j 后续也可以乱填了。
                        */
			if ((Y>>lg)&1) dp[lg][1][1]=1;
			dp[lg][1][0]=1;
		}
		else if (Y>=(1<<lg+1))
		{
                        /*
                            与上面那种情况 X 与 Y 的地位相反。
                        */
			if ((X>>lg)&1) dp[lg][1][1]=1;
			dp[lg][0][1]=1;
		}
                /*
                    如果 X 和 Y 的这一位都是 1,
                    谁填 1 都行,后续分别有一个人可以乱填。
                */
		else if (((Y>>lg)&1)&&((X>>lg)&1)) dp[lg][0][1]=dp[lg][1][0]=1;
                /*
                    最后,由于我们提前排除了都不够的情况,
                    现在是恰好 X 或 Y 有一个第 lg 位是 1。
                    当前位置只有一种方案,后续都不能乱填。
                */
		else dp[lg][0][0]=1;
                
                
                // 终于初始化完了,开始转移……
                // 用 x 表示 X 的第 t 位,y 表示 Y 的第 t 位。
                // 二元组 (a,b) 表示在 i 的第 t 位填上 a,在 j 的第 t 位填上 b。
		for (int t=lg-1;t>=0;--t)
		{
                        // 除了 x=y=1,其他情况都可以填 (x,y) 从 dp[t+1][0][0] 转移去 dp[t][0][0]
			dp[t][0][0]=((!(((X>>t)&1)&((Y>>t)&1)))*dp[t+1][0][0])%mod;
                        // 填 (x,0) 可以从 dp[t+1][0][1] 转移去 dp[t][0][1]
                        // 如果 x=0,填 (0,1) 也可以从 dp[t+1][0][1] 转移去 dp[t][0][1]
                        // 如果 y=1,填 (x,0) 可以从 dp[t+1][0][0] 转移去 dp[t][0][1]
			dp[t][0][1]=((1+(((X>>t)&1)!=1))*dp[t+1][0][1]+((Y>>t)&1)*dp[t+1][0][0])%mod;
                        // 与上一条式子的解释一样,x 和 y 的地位相反。
			dp[t][1][0]=((1+(((Y>>t)&1)!=1))*dp[t+1][1][0]+((X>>t)&1)*dp[t+1][0][0])%mod;
                        // 如果 x=y=1,填 (0,0) 可以从 dp[t+1][0][0] 转移去 dp[t][1][1]
                        // 如果 x=1,填 (0,0),(0,1) 可以从 dp[t+1][0][1] 转移去 dp[t][1][1]
                        // 如果 y=1,填 (0,0),(1,0) 可以从 dp[t+1][1][0] 转移去 dp[t][1][1]
                        // 填 (0,0),(0,1),(1,0) 可以从 dp[t+1][1][1] 转移去 dp[t][1][1]
			dp[t][1][1]=((((X>>t)&1)&((Y>>t)&1))*dp[t+1][0][0]+(((X>>t)&1))*2*dp[t+1][0][1]+(((Y>>t)&1))*2*dp[t+1][1][0]+3*dp[t+1][1][1])%mod;
		}
                //统计答案即可
		ans=(ans+(dp[0][0][0]+dp[0][1][0]+dp[0][0][1]+dp[0][1][1])%mod*(1+lg)%mod)%mod;
	}
	return ans;
}
int main()
{
	int T;
	for (scanf("%d",&T);T--;) 
		printf("%lld\n",solve());
	return 0;
}

Other

为什么我半梦半醒会想得出来这种题推得出来这种 DP 方程啊呜呜呜,醒着绝对不会做……