开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第19天,点击查看活动详情
【ICPC】2020上海站 C. Sum of Log | 动态规划、二进制、分类讨论
题目链接
题目
题目大意
对于给定的正整数 ,范围是 ,要求输出
其中:
- 表示位运算中的“与”运算。
- 中括号 的含义为:当表达式 的值为真时,,否则 。
- 表示不大于 的最大整数。
思路
熬了大通宵早八摸鱼半梦半醒写的 DP,提交一发AC……睡了一下午起来乍一看根本不知道自己在干什么(x
让窝比着代码胡乱分析(x
首先观察题目给出来的式子:
容易发现当 时 ,对答案没有贡献。
我们只需要考虑 和 没有相同的位置是 的情况,在此情况下 不会进位,所以 其实等价于 。也就相当于把我们枚举的 和 转化为二进制形式,取较长的长度。
我们枚举所有 和 所有可能的二进制下的长度,对于每一个可能的长度 ,我们使用 DP 来求解方案数。
我们选择的数字 和 分别不能超过 和 。在二进制形式下,自高位向低位试图选择 时:
- 当我们枚举到了第 位,如果存在更高位 是 , 填了 ,说明第 位可以乱选 ,都不会大于 。
- 如果不存在上述情况,说明我们从最高位到第 位,每个位置上的数字都与 相同,所以 的第 位选的数字不能超过 的第 位。
所以我们将 DP 状态设置为从最高位位到第 位选择的方案数,且在后续的转移中:
- : 和 均不能乱选。
- : 不能乱选, 可以乱选。
- : 可以乱选, 不能乱选。
- : 和 均可以乱选。
转移如下图所示:
不管怎么组织还是乱糟糟的……于是将 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 方程啊呜呜呜,醒着绝对不会做……