Files
python/TangDou/AcWing/ShuWei/1081.md
HuangHai 1f397eca87 'commit'
2025-08-30 18:35:01 +08:00

175 lines
6.9 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## [$AcWing$ $1081$ 度的数量](https://www.acwing.com/problem/content/1083/)
### 一、题目描述
求给定区间 $[X,Y]$ 中满足下列条件的整数个数:这个数恰好等于 $K$ 个互不相等的 $B$ 的整数次幂之和。
例如,设 $X=15,Y=20,K=2,B=2$,则有且仅有下列三个数满足题意:
$17=2^4+2^0$
$18=2^4+2^1$
$20=2^4+2^2$
**输入格式**
第一行包含两个整数 $X$ 和 $Y$,接下来两行包含整数 $K$ 和 $B$。
**输出格式**
只包含一个整数,表示满足条件的数的个数。
**数据范围**
$1≤X≤Y≤2^{31}−1$,$1≤K≤20$,$2≤B≤10$
**输入样例**:
```cpp {.line-numbers}
15 20
2
2
```
**输出样例**:
```cpp {.line-numbers}
3
```
### 二、数位$DP$知识
数位$dp$的题目一般会问 : <font color='red' size=4><b>指定区间内,满足某种性质的数的个数</b></font>。
**解题技巧**:
* 利用 **前缀和**,比如求区间$[x,y]$中的 **个数**,转化成求$[0,y]$的**个数** -$[0,x-1]$的**个数**
### 三、解题思路
题目就是在一个区间$[l,r]$内,**找出有多少个符合题意的数**,这里的符合题意是指:这个数的 **$b$进制** 表示中,其中有$k$个位置上是$1$、其他位上全是$0$。
比如样例中$B=2$,$K=2$,这个数是$18$.就是把$18$化为**二进制**,$18$ 化为二进制数为:$10010$,其中有$2$位$1$,其他位都是$0$
再比如 $B=3$,$K=2$,这个数是$12$.就是把$12$化为**三进制**,$12$化为三进制为$110$ ,其中也有$2$个$1$,其余都是$0$.
#### 暴力法
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int l, r; // 数位DP的区间范围
int k, b; // k:个进制位为1,b进制
int a[N], al; // 用来分解b进制的数组
// 将x转化为b进制
void calc(int x, int b) {
al = 0;
memset(a, 0, sizeof a);
while (x) a[++al] = x % b, x /= b;
}
/*
通过了 7/14个数据
一半的分数也是OIer的梦想!
*/
int res;
int main() {
cin >> l >> r >> k >> b;
// 1、将[l,r]之间所有数字进行b进制分解
for (int i = l; i <= r; i++) {
calc(i, b);
int cnt = 0;
bool bad = false; // 是不是全是0或1,一旦是bad=true,表示出现了大于数字1的数位,此时,一票否决~!
// 2、检查是不是存在大于1的,大于1的是无效数字,跳过
for (int j = 1; j <= al; j++) {
if (a[j] > 1) {
bad = true;
break;
}
// 3、统计一下1出现的次数
if (a[j] == 1) cnt++;
}
// 4、判断出现的次数是不是等于k,如果是,那么找到了一个答案,计数
if (!bad && cnt == k) res++;
}
printf("%d\n", res);
return 0;
}
```
### 五、$dfs$数位$dp$模板
**[数位$dp$总结之从入门到模板 ](https://www.cnblogs.com/littlehb/p/15796143.html)**
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 32; // 输入的数据范围2^31-1,也就是整数上界。2进制是最小的进制,32也够了
int l, r; // 数位DP的区间范围
int k, b; // k:个进制位为1,b进制
int a[N], al; // 用来分解b进制的数组
int f[N][N]; // f[i][j] i:枚举到的位置 j:已经计算完1的个数,这里需要一点DP的状态描述的思想,只有一维记录到哪了,是无法描述整个当时场景的
/**
*
* @param u 当前枚举到的数位
* @param st 数字1出现的次数
* @param op 是否贴上界
* @return 在当前情况下,它 后续 符合条件的一共有多少个数
*/
int dfs(int u, int st, bool op) {
// 到最后一位数位,是否恰好有k个不同的1,是:贡献价值+1,否:贡献价值+0
if (u == 0) return st == k;
if (op == 0 && ~f[u][st]) return f[u][st]; // 不贴上界,并且计算过,返回结果
/*
① 不贴着上界,可以将此区间算满,缓存起来,不用重复计算
② 贴上界,每次重新计算
本题要求:
(1)如果op = 1, 贴上界
如果a[u]=0,那么up最大是0,否则,up最大是1,简写成min(a[u],1)
(2)如果op = 0, 不贴上界,可以选择的范围[0,1],up最大是1
*/
int ans = 0;
int up = op ? min(a[u], 1) : 1; // up就是你的极限!本题,这句是一个精华点
for (int i = 0; i <= up; i++) { // 枚举上限之内的数字(其实,也只有0和1有机会,这还要看up的取值。up=0,跑一次循环;up=1,跑两次循环)
// 如果i==0,则st+i=st,st不会变大,也不会超过k,此句无影响后面的代码执行。
// 如果i==1,则表示多选择了一个是1的数位,st+1,有可能已经超过了k的限制,再向后跑深搜就没有了意义,及时止损,剪枝
if (st + i > k) continue; // 剪枝更健康
// u-1:继续向低一位进行前进
// st+i:1的个数变化情况,如果i==0,则表示u这个数位没有选择数字1;如果i==1,则表示u这个数位选择了数字1,不断的向后传递这个信息,用于判断是不是符合条件的数字
// op && i == a[u] :此参数的含义是是否贴合上限
// op:原来是贴上界的 && i == a[u]: 当前位取值也是贴上界
// 那么,下面一位是继承前面的结果关系,也就是,它也在贴上界的情况下进行讨论
// ==的运算优先级 高于 && ,所以先判断 i == a[u]是第一步,与 op 相 && 是第二步
ans += dfs(u - 1, st + i, op && i == a[u]);
}
// 只记录不受上限限制的个数,受上限限制的每次重新计算
// 为什么只记录不贴上界的结果,贴上界的为什么不能直接使用结果?
// 这是因为:如果第一位贴了上界,那么第二位也可能继续贴上界,导致后续的无法取完整,无法直接使用结果
if (!op) f[u][st] = ans;
/*这里对应上面的记忆化,在一定条件下时记录,保证一致性,当然如果约束条件不需要考虑lead,这里就是lead就完全不用考虑了*/
return ans;
}
// 采用类似于前缀和的思想,所以,需要封装成一个calc函数,计算两次,然后取差值
int calc(int x) {
al = 0; // 临时数组a初始化,这里挺有意思,把al=0就相当于全新初始化掉了a,不用采用memset(a,0,sizeof a);
while (x) a[++al] = x % b, x /= b; // 把x按照进制分解到数组中,下标从1开始
return dfs(al, 0, true); // 从最高位开始,目前数字1出现了0次,贴上界
}
int main() {
memset(f, -1, sizeof f); // 初始化
cin >> l >> r >> k >> b; // 边界,k个数位是1,b进制
// 前缀和
cout << calc(r) - calc(l - 1) << endl;
return 0;
}
```