算法杂记 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. [1, 2, 3][1, 3, 2]
  3. [1, 2, 3, 1][1, 3, 2]
  4. [1, 2, 3][1, 3, 2, 1]
  5. [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]\)

然后使用两个数组 leftright 维护两个数组中元素是否出现

然后使用 diff 表示 leftright 之间不同的个数,当且仅当 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;
}
posted @ 2023-02-14 21:30  Last_Whisper  阅读(19)  评论(0编辑  收藏  举报