ICPC20上海C——小白也能秒懂的数位dp讲解
Posted hans774882968
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ICPC20上海C——小白也能秒懂的数位dp讲解相关的知识,希望对你有一定的参考价值。
dp定义有很多种。采用这个最直观的:添加链接描述
i&j=0,意味着相加不会进位。log2(i+j)+1向下取整,意味着它等于i、j中的最长len值。
接下来为了解决原问题,我们定义一个更为简单的子问题为:
∑
i
=
0
X
∑
j
=
[
i
=
0
]
Y
[
i
&
j
=
0
]
\\sum_{i=0}^X \\sum_{j=[i=0]}^Y [i\\&j=0]
i=0∑Xj=[i=0]∑Y[i&j=0]
为什么做此定义?以up1=up2=1(注:对应已知的数位dp模板的up变量,比如hdu2089,见下文“附”)为例:枚举i和j的最高位(使得最高位固定),当两者最高位均为0,则取原问题。否则
[
l
o
g
2
(
i
+
j
)
+
1
]
[log_2(i+j)+1]
[log2(i+j)+1]
已经确定(“[]”表示向下取整),在我的代码里就是idx+1,于是可以把它提到求和式外面,这样就构造出上述更为简单的子问题。
所以我们定义dp[idx,zero]为:当前考虑到下标为idx的位,求的是原问题/子问题(zero=1/0)。之所以变量名叫zero,只是因为一个巧合:该变量恰好和表示i和j的更高位是否全都是0的变量是同一个。
因为原问题的转移方程里有可以提取的公因子,所以我们定义一个权值weight,它等于1或idx+1。下面看weight什么时候等于idx+1。由定义可以知道,如果zero=0,则无论如何权值都只乘1;如果zero=1,表示求解原问题,则i和j不都等于0时,weight=idx+1。
伪代码
LL ans = 0;
int up1 = f1 ? x[idx] : 1,up2 = f2 ? y[idx] : 1;
rep(i,0,up1){
rep(j,0,up2){
if(i & j) continue;
int weight = zero && (i || j) ? idx+1 : 1;
(ans += dfs(idx-1,f1 && i == up1,f2 && j == up2,NXTZERO) * weight % mod) %= mod;
}
}
接下来确定NXTZERO。由以上讨论已经知道,zero=1(求解原问题)且i和j都为0时,NXTZERO=1。其他情况NXTZERO=0。
NXTZERO = zero && !(i || j)
- 因为dp定义与test case无关,所以memset只需要在最开始做1次,这一技巧在hdu2089中也用到。
- 需要减1,因为数位dp的代码把i == 0 且 j == 0的情况也算进去了。
参考已有数位dp模板,有如下代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
#define rep(i,a,b) for(int i = (a);i <= (b);++i)
#define re_(i,a,b) for(int i = (a);i < (b);++i)
#define dwn(i,a,b) for(int i = (a);i >= (b);--i)
const int N = 40;
const int mod = 1e9 + 7;
int x[N],y[N];
LL dp[N][2];
void dbg(){puts("");}
template<typename T, typename... R>void dbg(const T &f, const R &... r) {
cout << f << " ";
dbg(r...);
}
template<typename Type>inline void read(Type &xx){
Type f = 1;char ch;xx = 0;
for(ch = getchar();ch < '0' || ch > '9';ch = getchar()) if(ch == '-') f = -1;
for(;ch >= '0' && ch <= '9';ch = getchar()) xx = xx * 10 + ch - '0';
xx *= f;
}
LL dfs(int idx,int f1,int f2,bool zero){
if(idx == -1) return 1;
LL &d = dp[idx][zero];
if(~d && !f1 && !f2) return d;
LL ans = 0;
int up1 = f1 ? x[idx] : 1,up2 = f2 ? y[idx] : 1;
rep(i,0,up1){
rep(j,0,up2){
if(i & j) continue;
int weight = zero && (i || j) ? idx+1 : 1;
(ans += dfs(idx-1,f1 && i == up1,f2 && j == up2,zero && !(i || j)) * weight % mod) %= mod;
}
}
if(!f1 && !f2) d = ans;
return ans;
}
LL solve(int u,int v){
int l1 = 0,l2 = 0;
memset(x,0,sizeof x);memset(y,0,sizeof y);
for(;u;u >>= 1) x[l1++] = u & 1;
for(;v;v >>= 1) y[l2++] = v & 1;
return (dfs(max(l1,l2)-1,1,1,true) + mod-1) % mod;
}
int main(int argc, char** argv) {
memset(dp,-1,sizeof dp);
int T;read(T);
while(T--){
int u,v;read(u);read(v);
printf("%lld\\n",solve(u,v));
}
return 0;
}
可惜它TLE了。这就引出一个优化:定义dp[idx,f1,f2,zero],其中f1和f2是以上代码的f1和f2。它们不实际参与决策,但放在状态里。这是一个空间换时间的技巧。那么
if(~d && !f1 && !f2) return d;
if(!f1 && !f2) d = ans;
就变成了
if(~d) return d;
return d = ans;
相应地,必须在每个case都memset一次。
这就得到我们在参考链接中看到的4维dp,本文指出它本质上是2维的dp+空间换时间技巧。这样就得到一个性能更优(可以AC)的代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
#define rep(i,a,b) for(int i = (a);i <= (b);++i)
#define re_(i,a,b) for(int i = (a);i < (b);++i)
#define dwn(i,a,b) for(int i = (a);i >= (b);--i)
const int N = 40;
const int mod = 1e9 + 7;
int x[N],y[N];
LL dp[N][2][2][2];
void dbg(){puts("");}
template<typename T, typename... R>void dbg(const T &f, const R &... r) {
cout << f << " ";
dbg(r...);
}
template<typename Type>inline void read(Type &xx){
Type f = 1;char ch;xx = 0;
for(ch = getchar();ch < '0' || ch > '9';ch = getchar()) if(ch == '-') f = -1;
for(;ch >= '0' && ch <= '9';ch = getchar()) xx = xx * 10 + ch - '0';
xx *= f;
}
LL dfs(int idx,int f1,int f2,bool zero){
if(idx == -1) return 1;
LL &d = dp[idx][f1][f2][zero];
if(~d) return d;
LL ans = 0;
int up1 = f1 ? x[idx] : 1,up2 = f2 ? y[idx] : 1;
rep(i,0,up1){
rep(j,0,up2){
if(i & j) continue;
int weight = zero && (i || j) ? idx+1 : 1;
(ans += dfs(idx-1,f1 && i == up1,f2 && j == up2,zero && !(i || j)) * weight % mod) %= mod;
}
}
return d = ans;
}
LL solve(int u,int v){
int l1 = 0,l2 = 0;
memset(x,0,sizeof x);memset(y,0,sizeof y);memset(dp,-1,sizeof dp);
for(;u;u >>= 1) x[l1++] = u & 1;
for(;v;v >>= 1) y[l2++] = v & 1;
return (dfs(max(l1,l2)-1,1,1,true) + mod-1) % mod;
}
int main(int argc, char** argv) {
int T;read(T);
while(T--){
int u,v;read(u);read(v);
printf("%lld\\n",solve(u,v));
}
return 0;
}
附:hdu2089的AC代码
2维且memset只做1次的版本
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
#define rep(i,a,b) for(int i = (a);i <= (b);++i)
#define re_(i,a,b) for(int i = (a);i < (b);++i)
#define dwn(i,a,b) for(int i = (a);i >= (b);--i)
const int SZ = 20;
int dl,d[SZ],dp[SZ][2];
template<typename Type>inline void read(Type &xx){
Type f = 1;char ch;xx = 0;
for(ch = getchar();ch < '0' || ch > '9';ch = getchar()) if(ch == '-') f = -1;
for(;ch >= '0' && ch <= '9';ch = getchar()) xx = xx * 10 + ch - '0';
xx *= f;
}
int dfs(int idx,bool is6,bool isup){
if(idx == -1) return 1;
if(!isup && ~dp[idx][is6]) return dp[idx][is6];
int up = isup ? d[idx] : 9;
int ans = 0;
rep(i,0,up){
if(i == 4) continue;
if(is6 && i == 2) continue;
ans += dfs(idx - 1,i == 6,isup && i == up);
}
if(!isup) dp[idx][is6] = ans;
return ans;
}
int solve(int x){
dl = 0;
for(;x;x 以上是关于ICPC20上海C——小白也能秒懂的数位dp讲解的主要内容,如果未能解决你的问题,请参考以下文章
45届ICPC亚洲区域赛(上海)C.Sum of Log(卡常数位dp)
The 2018 ACM-ICPC上海大都会赛 J Beautiful Numbers (数位DP)