题解:P17729 [SCP-J 2026] 进位 / carry
——是赛后补题咕。
本题解同步发表于 博客园。
题目传送门。
用老师 10RMB 巨资买下览遍千秋老师的讲课直播写出的做法。
Luogu P17729 [SCP-J 2026] 进位 / carry 题解
:::info[题目简述]
给定一个长度为
对于区间
- 令
S=0 。 - 按照
A_l,A_{l+1},\ldots,A_r 的顺序,依次将S 的值增加A_i 。
所有加法均按照普通的二进制加法进行,用
若一个长度为
-
- 对任意
1\le i\le m ,均满足1\le p_i\le n 。 - 对任意
1\le i< m ,均满足p_i<p_{i+1} 且C(p_i,p_{i+1})=1 。
两个数组不同,当且仅当两个数组长度不同,或至少一个位置的数字的不同。
求有多少个不同的合法数组,答案对
| 注意:本题输入量较大,建议选手使用较快的读入方式,以避免因输入耗时影响程序效率。 |
|---|
前置知识
首先我们要知晓二进制加法的性质:在二进制加法下,每产生一次进位,二进制就会减少一个
:::info[证明]
- 若一个二进制数位为
1 且加上了1 (共两个),这一位将会变成0 并将1 进位到下一位(共一个),总计减少了一个1 。 - 若一个二进制数位是
0 且加上了1 (共一个),这一位将会变成1 且没有产生贡献(共一个),总计不变。 - 若一个二进制数位是
0 且加上了0 ,显然不会进位也不会变化,总计不变。
对于进位使得原本不需要进位的数产生进位,可以视作额外进行若干次加法,等价于直接相加,上述结论依然成立。证毕。
因此我们记
:::
初步
显然这是统计方案数,那么我们容易想到用 DP 处理转移。
定义
先考虑部分分做法,我们可以查找每一个数对
时间复杂度
预估应该可以拿下前两个测试点。
优化
考虑原题中
对于每个
将
两两相消,得到
形式化的讲,即:
对于
对于
于是我们压掉了一维,现在时间复杂度
正解
这个时间复杂度还是炸掉了呢。考虑把枚举区间优化掉。
对于
也可以感性理解:加的数越多,进位越多;加的数越少,进位越少。
形式化地,有:
- 锚定右端点
r ,左端点l 向右移,C(l,r) 越来越小。 - 锚定左端点
l ,右端点r 向右移,C(l,r) 越来越大。
那么当
于是,所有合法的左端点一定连成一片。
:::info[证明]
采用反证法,假设存在一个
那么由于
综上,
:::
维护
那么所有合法左端点构成滑动区间
状态转移方程变成
其中
这个也可以再加上一个
因此我们维护总共三个指针
可以
时间复杂度非常优秀,可以拿到满分。
:::success[AC Code]
#include<bits/stdc++.h>
using namespace std;
#define int long long //Ai>=10^12.
#define cnt(x) (__builtin_popcountll(x)) //cnt 函数在这里呢咕
#define endl '\n'
const int N=5e5+7;
const int mod=998244353;
//快读快写
inline int read(){
int x=0,f=1;
char c=getchar();
while(!isdigit(c)){if(c=='-')f=-1; c=getchar();}
while(isdigit(c)){x=(x<<1)+(x<<3)+(c-'0'),c=getchar();}
return x*f;
}
inline void write(int x){
if(x<0)x*=-1,putchar('-');
if(x>9)write(x/10);
putchar(x%10+'0');
}
int n,T;
//dp 统计方案数
int a[N],dp[N];
//pre 统计原数组前缀和,prec 统计 cnt 前缀和,predp 统计 dp 前缀和
int pre[N],prec[N],predp[N];
//即题面中的 C(l,r) 函数,为了区分和寓意吉利(bush。
inline int AC(int l, int r){
return prec[r]-prec[l-1]-cnt(pre[r]-pre[l-1]);
//见上。
}
signed main(){
T=read();
while(T--){
memset(pre,0,sizeof(pre));
memset(prec,0,sizeof(prec));
memset(predp,0,sizeof(predp));
//多测清空。
n=read();
for(int i=1; i<=n; i++){
a[i]=read();
pre[i]=pre[i-1]+a[i];
prec[i]=prec[i-1]+cnt(a[i]);
}
//cl 表示合法左界,cr 表示合法右界,r 表示右端点,ans 用于统计答案。
int cl=1,cr=1,r=1, ans=0;
while(r<=n){
while(cl<r && AC(cl,r)>1) cl++;//处理合法界限,见上。
while(cr<r && AC(cr,r)>0) cr++;
dp[r]=(1+predp[cr-1]-predp[cl-1]+mod)%mod;//记得加上一个模数防止出现负数。
predp[r]=(predp[r-1]+dp[r]+mod)%mod;
r++;
}
for(int i=1; i<=n; i++) ans=(ans+dp[i]+mod)%mod;//记得取模
write(ans);
putchar('\n');
}
return 0;
}
// Code by cao2333, [email protected]
| 评测记录 |
|---|
后记
感谢览遍千秋老师的讲解。这道题其实还有一个小细节:二进制下加法进位个数与加法顺序无关,且多数加和进位同样可以用公式计算,可以根据 出题人拼尽全力无法卡掉。
注意事项:
- 取模时一定要加上一个模数防止出现负数。
- 不要 给涉及
C(l,r) 的计算的任何数据取模(包括A,pre,prec )。 -
- 观察左界与右界的区别,检查初始值,检查有没有
dp_r+1 。