【每日一题】补档 ARC136D - Without Carry | 子集DP | 困难

题目内容

原题链接

给定一个长度为 n n n 的整数数组 a a a ,问有多少个不同的下标对 i i i j j j 满足 a i + a j a_i+a_j ai+aj 的加法中没有进位。

数据范围

2 ≤ n ≤ 1 0 6 2\leq n\leq 10^6 2n106
0 ≤ a i < 1 0 6 0\leq a_i <10^6 0ai<106

题解

十进制的子集DP

f [ j ] [ i ] f[j][i] f[j][i] 表示前 j j j 位(第1位是个位,第2位是十位)的数 i i i 的子集

  • 考虑前 j − 1 j-1 j1
    f [ j ] [ i ] = f [ j − 1 ] [ i ] f[j][i]=f[j-1][i] f[j][i]=f[j1][i]
  • 考虑第 j j j 位的数 i i i 的子集
    i − 1 0 j − 1 , i − 2 × 1 0 j − 1 , . . . i-10^{j-1}, i-2\times 10^{j-1},... i10j1,i2×10j1,...
    需要考虑的是 i − 1 0 j − 1 i- 10^{j-1} i10j1 的子集也是 i i i 的子集,所以只需要加上 i − 1 0 j − 1 i-10^{j-1} i10j1 的子集即可。
    f [ j ] [ i ] = f [ j ] [ i ] + f [ j ] [ i − 1 0 j − 1 ] f[j][i]=f[j][i]+f[j][i-10^{j-1}] f[j][i]=f[j][i]+f[j][i10j1]

因为每次只用到前一位的状态,所以可以直接滚动数组优化。

时间复杂度: O ( 6 n ) O(6n) O(6n) 6 6 6 是因为值域至多是 6 6 6 位数

代码

#include <bits/stdc++.h>
using namespace std;

const int MAX = 1000000;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;
    vector<int> a(n);
    vector<long long> f(MAX);

    for (int i = 0; i < n; ++i) {
        cin >> a[i];
        f[a[i]] += 1;
    }

    // f[j][i] 表示只考虑十进制的前 j 位 (个位是第一位),i 的子集的数量
    // f[j][i] = f[j - 1][i]
    // 那么只考虑第 j 位
    // 对于第 j 位,i-10^{j-1}, i-2*10^{j-1} ... 都是其子集
    // 但是 f[i-10^{j-1}] 的子集也是 f[i][j] 的子集,所以只考虑这个即可
    // f[j][i] += f[j][i-10^{j-1}]
    for (int j = 1; j < MAX; j *= 10) {
        for (int i = 0; i < MAX; ++i) {
            if (i / j % 10 > 0) {
                f[i] += f[i - j];
            }
        }
    }

    long long ans = 0;
    for (int x: a) {
        ans += f[999999 - x];
        bool ok = true;
        while (x > 0) {
            if (x % 10 > 4) {
                ok = false;
                break;
            }
            x /= 10;
        }
        if (ok) ans -= 1;
    }

    cout << ans / 2 << "\n";

    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
首先,我们需要将该文法转换为可以进行处理的形式,例如转换为无回溯的上下文无关文法(CFG): S -> AS | b A -> SA | a 可以转换为: S -> AS | b A -> aA1 | ε A1 -> SA 接下来,我们可以使用子集构造法来生成 DFA。我们需要建立两个状态集合:当前状态集合和下一个状态集合。初始状态集合为文法开始符号 S 的闭包。 状态转换的条件是:如果在当前状态集合中存在符号 X 的某个后继符号,则将其加入到下一个状态集合中。下一个状态集合是当前状态集合的所有后继符号的闭包。 我们可以使用以下步骤来构造 DFA: 1. 确定文法符号集合:{S, A, A1, a, b}。 2. 确定状态集合: - 初始状态集合为 {S, A, A1} 的闭包,即 {S, A, A1}. - 其他状态集合是从初始状态集合开始通过后继符号进行闭包操作得到的。 3. 确定接受状态集合:包含 S 和 A 的状态。 4. 确定转移函数:对于每个状态集合和每个输入符号,计算它的后继状态集合,直到没有新的状态集合出现。我们可以使用表格来表示转移函数。 下面是构造出来的 DFA: | 状态 | a | b | | ---- | - | - | | {S,A,A1} | {A1} | {S,A,A1} | | {A1} | {S,A,A1} | {S,A,A1} | | {S,A} | {A1} | {S,A,A1} | | {A} | {A1} | {S,A,A1} | | ∅ | ∅ | ∅ | 其中,∅ 表示空状态集合。最终的 DFA 如下图所示: ``` a b →S,A,A1 ← S,A,A1 ↓ ↑ ↓ ↑ A1, ←→ S,A,A1 a b ``` 因此,我们得到了识别文法活前缀的 DFA。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值