算法杂记 2023/02/14
算法杂记 2023/02/14
P2602 数字计数(数位DP)
题目描述
给定两个正整数 \(a\) 和 \(b\),求在 \([a,b]\) 中的所有整数中,每个数码(digit)各出现了多少次。
输入格式
仅包含一行两个整数 \(a,b\),含义如上所述。
输出格式
包含一行十个整数,分别表示 \(0\sim 9\) 在 \([a,b]\) 中出现了多少次。
样例 #1
样例输入 #1
1 99
样例输出 #1
9 20 20 20 20 20 20 20 20 20
提示
数据规模与约定
- 对于 \(30\%\) 的数据,保证 \(a\le b\le10^6\);
- 对于 \(100\%\) 的数据,保证 \(1\le a\le b\le 10^{12}\)。
数位DP的模板题。
我们创建一个对应的模板:
T dfs(T len, T is_small, T sum, T zero, T digit)
其中各符号代表的意思为:
len
当前对应的位数is_small
代表当前是否会比上限的小sum
对于当前digit
总共出现的次数zero
是否有前导零digit
当前统计的是什么位
然后就可以直接转移了,思路很简单,具体看代码。
#include <bits/stdc++.h>
using namespace std;
#define DEBUG 0
using ll = long long;
const int N = 15;
// f[i][issmall][sum][zero]
int n; // length
int nums[N];
ll f[N][2][N][2];
ll dfs(int len, bool issmall, ll sum, bool zero, int dig){
if (len == 0) return sum;
if (f[len][issmall][sum][zero] != -1)
return f[len][issmall][sum][zero];
ll ret = 0;
for (int i = 0; i < 10; ++ i){
if (!issmall && (i > nums[len]))
break;
ret += dfs(len - 1, issmall || (i < nums[len]), sum + ((i == dig) && (!zero || i)), zero && (i == 0), dig);
}
return f[len][issmall][sum][zero] = ret;
}
ll solve(ll x, int dig){
int i = 0;
while (x){
nums[++i] = x % 10;
x /= 10;
}
memset(f, -1, sizeof(f));
return dfs(i, 0, 0, 1, dig);
}
signed main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
ll a, b;
std::cin >> a >> b;
for (int i = 0; i < 10; ++ i)
std::cout << solve(b, i) - solve(a - 1, i) << " \n"[i == 9];
return 0;
}
可行的观光方案(思路,双指针)【华为质量检测】
总结下,题目意思是给定一个数组
arr
,其中arr[i]
代表某个景点类型,数组长度为 \(n\)。现在需要我们统计两个子数组 \([1, i]\) 和 \([j, n]\) ,然后子数组去重后集合相同的可能情况。
比如数组是
arr = [1, 2, 3, 1]
,最后可行答案:
[1]
和[1]
[1, 2, 3]
和[1, 3, 2]
[1, 2, 3, 1]
和[1, 3, 2]
[1, 2, 3]
和[1, 3, 2, 1]
[1, 2, 3, 1]
和[1, 3, 2, 1]
一共是 \(5\) 个。
还一个样例:
[5, 3, 6, 5, 3]
答案是10
。
- \(1\le arr.length \le 10^5\)
- \(-10^5 \le arr.length \le 10^5\)
- 集合相等:两个集合的完全相同就是相等,只要有一个元素不相同就是不想等。
我们需要用双指针 \(l, r\),分别维护:\([1, l]\) 和 \([r, n]\)。
然后使用两个数组 left
和 right
维护两个数组中元素是否出现。
然后使用 diff
表示 left
和 right
之间不同的个数,当且仅当 diff == 0
才代表左右两个集合是相同的。(利用 diff
和数组来快速表示两边元素的集合是否相同,否则如果用 set
时间复杂度太高了。)
每次 \(l\) 往右扩展,右边 \([r, n]\) 区间对应进行更新。我们可以扩展到所有可行的 \(r\),直到出现了一个 left
不存在的元素。
如果 \(l\) 往右扩展但是 diff
并没有改变,那么当前的 \(l\) 和上一个 \(l^\prime\) 可行的答案是相同的,所以我们使用 prvAns
用来标记上一个 \(l\) 对应的答案,然后可以直接加上 prvAns
。
这样的算法的时间复杂度为:\(O(n)\)。
long long sol(const vector<int>& attractions){
long long ans = 0;
int n = attractions.size();
const int MAXN = 1e5 + 5;
int diff = 0;
vector<int> left(2 * MAXN, 0), right(2 * MAXN, 0); // 统计左右子数组出现个数
// map<int, int> left, right;
int j = n; // 右子数组的左端点
long long prvAns = 0; // 上一个答案
for (int i = 0; i < n; ++ i){
int cur = attractions[i] + MAXN;
if (left[cur]) { // 如果和上一个相同
ans += prvAns;
continue;
}
left[cur] = 1; // 更新,之前肯定是 left[cur] = 0
// 更新diff
if (right[cur] != left[cur]) ++ diff;
else if (right[cur] == left[cur]) -- diff;
int curAns = 0;
while (j - 1 >= 0){
// 必须保证左边得有他
if (left[attractions[j - 1] + MAXN]){
if (right[attractions[j - 1] + MAXN] == 0) -- diff;
right[attractions[j - 1] + MAXN] = 1;
if (diff == 0){
++ curAns;
}
j -= 1;
}
else break;
}
ans += curAns;
prvAns = curAns;
}
return ans;
}