[EC Final 2022] Best Carry Player 2

GitHub跳转原题关系图返回列表

把进位看成数位上的选择:每位取刚好够用的最小数字,用「当前进位状态 + 已用进位次数」的 DP 求最小 y,答案可达 10^35 需用 128 位整数。

OJ: luogu

题目 ID: P9718

难度:提高

标签:数位 DP动态规划构造数学思维

创建: 2026-10-02 15:23

更新: 2026-10-03 12:37

形式化题目

把 xx 从低位到高位写成 a0,a1,…,an−1a_0, a_1, \ldots, a_{n-1}(a0a_0 是个位,n⩽18n \leqslant 18),把 yy 的同位数字写成 bib_i,把第 ii 位向第 i+1i+1 位的进位记作 ci+1c_{i+1},其中 c0=0c_0 = 0。竖式加法的规则就是

ci+1=1  ⟺  ai+bi+ci⩾10.c_{i+1} = 1 \iff a_i + b_i + c_i \geqslant 10 .

要求进位次数 ∣{i:ci+1=1}∣|\{i : c_{i+1} = 1\}| 恰好等于 kk,在所有满足条件的正整数 y=∑ibi10iy = \sum_i b_i 10^i 中求最小值;不存在满足条件的 yy 时输出 −1-1。

注意 yy 可以比 xx 长,此时更高位的 aia_i 视为 00;最高位之外悬挂的最终进位不计入次数。

以样例第三、四组为例:

  • x=12345678, k=18x = 12345678,\ k = 18,答案 y=999999999987654322y = 999999999987654322,此时每一位都在进位,并把进位链一路延伸到 xx 之外;
  • x=990099, k=5x = 990099,\ k = 5,答案 y=9910y = 9910:990099+9910=1000009990099 + 9910 = 1000009,进位发生在第 1∼51 \sim 5 位,其中第 4,54,5 位是 99,靠 b4=b5=0b_4 = b_5 = 0 白拿进位。

暴力解法

思路

最直接的做法就是从小到大枚举 y=1,2,3,…y = 1, 2, 3, \ldots,对每个 yy 做一次竖式模拟:用一个变量 c 记录进位、cnt 记录进位次数,只要 xx 或 yy 还有位就继续,检查 x % 10 + y % 10 + c 是否达到 1010;达到就 cnt++ 且 c = 1,否则 c = 0。第一次 cnt == k 时输出这个 yy。

循环结束后若还剩进位,那是最高位之外的悬挂进位,题目不计入,所以不再额外加一。brute.cpp 把枚举上限设为 LIMIT,超过上限仍未找到就输出 -1;这在本题的大答案场景下会给出错误结果,因此它只能作为小数据基线。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-03 12:31
 * update_at: 2026-10-03 12:31
 */
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
//
// 直接从小到大枚举 y = 1, 2, 3, ...,
// 用竖式加法逐位模拟 x + y,统计 a[i] + b[i] + c 达到 10 的次数,
// 第一次恰好等于 k 的 y 就是答案。
// 注意:最高位之外的悬挂进位不算「进位」,所以只在两数都处理完之前统计。
// 因为答案可能很大,枚举上限取 LIMIT;配套的 gen.py 只生成答案远小于 LIMIT 的小数据。
#include <bits/stdc++.h>
using namespace std;

const long long LIMIT = 2000000;   // 枚举上限,保证对拍数据足够小

// 统计 x + y 竖式加法的进位次数
int count_carry(long long x, long long y) {
    int c = 0;        // 当前进位
    int cnt = 0;      // 已经发生的进位次数
    while (x > 0 || y > 0) {
        int s = (int)(x % 10) + (int)(y % 10) + c;
        if (s >= 10) {
            c = 1;
            cnt++;
        } else {
            c = 0;
        }
        x /= 10;
        y /= 10;
    }
    // 循环结束后剩下的进位是最高位的悬挂进位,题目不计入
    return cnt;
}

void solve_one(long long x, int k) {
    for (long long y = 1; y <= LIMIT; y++) {
        if (count_carry(x, y) == k) {
            printf("%lld\n", y);
            return;
        }
    }
    puts("-1");
}

int main() {
    int T;
    scanf("%d", &T);
    while (T--) {
        long long x;
        int k;
        scanf("%lld %d", &x, &k);
        solve_one(x, k);
    }
    return 0;
}

复杂度

设答案为 AA,xx 的位数为 nn,则时间 O(A⋅n)O(A \cdot n),额外空间 O(1)O(1)。

瓶颈

瓶颈不在模拟,而在「枚举 yy 的值」这件事和约束没有直接关系。进位次数是加在数位上的条件,而 yy 的空间大得离谱:

  • x=12345678,k=18x = 12345678, k = 18 时答案是 999999999987654322999999999987654322,枚举要 101810^{18} 次;
  • x=1017,k=18x = 10^{17}, k = 18 时答案的位数约 3535,更不可能枚举;
  • 每组测试都要重来一次,而 TT 可达 10510^5。

所以要换一个搜索对象:不再问「yy 取多少」,而问「哪些位置产生进位」。

正解

思路

固定 aia_i 和进入这一位的进位 cic_i,那么「这一位要不要产生进位」一旦决定,最优的 bib_i 就被唯一确定:为了让 yy 最小,bib_i 应该刚好够用,不用多花一分。四种情况如下。

进入进位 cic_i 这一位产生进位 存在条件 最优 bib_i 离开进位 ci+1c_{i+1}
00 否 无 00 00
00 是 ai⩾1a_i \geqslant 1 10−ai10 - a_i 11
11 否(进位结束) ai⩽8a_i \leqslant 8 00 00
11 是(进位继续) 无 9−ai9 - a_i 11

两处条件值得单独说明:

  • ai=0a_i = 0 时不能凭空产生进位。即使取 bi=9b_i = 9,0+9=90 + 9 = 9 也进不了位,所以每个进位段的首位必须满足 ai⩾1a_i \geqslant 1,此时取 bi=10−aib_i = 10 - a_i 恰好把和顶到 1010。
  • ai=9a_i = 9 时进位停不下来。9+bi+1⩾109 + b_i + 1 \geqslant 10 对一切 bi⩾0b_i \geqslant 0 都成立,无法在这一位结束进位,只能继续。这正是 xx 里连续的 99 能「白拿」进位的原因(此时 bi=9−9=0b_i = 9 - 9 = 0)。

于是问题变成:在数位上选出恰好 kk 个进位位置,使 ∑bi10i\sum b_i 10^i 最小。由于进位只依赖相邻位,可以做数位 DP。设 dp[c][j]dp[c][j] 表示处理完当前位后、当前进位状态为 cc、已经产生 jj 次进位时 yy 的最小值,初始 dp[0][0]=0dp[0][0] = 0,按上表从低位向高位转移,第 ii 位的代价乘以权值 10i10^i。

处理完 xx 的 nn 位之后还要收尾,因为第 n−1n-1 位可能产生了进位 cn=1c_n = 1:

  • dp[0][k]dp[0][k] 直接是候选答案:进位已经停下,且总共恰好 kk 次;
  • 若 dp[1][j]dp[1][j] 有效,进位还在往高位走。此时 ai=0 (i⩾n)a_i = 0\ (i \geqslant n),取 bi=9b_i = 9 就有 0+9+1=100 + 9 + 1 = 10,可以再记一次进位。还差 rem=k−jrem = k - j 次进位,就要在第 n∼n+rem−1n \sim n+rem-1 位都取 99,代价为
9⋅(10n+10n+1+⋯+10n+rem−1)=10n+rem−10n.9 \cdot (10^n + 10^{n+1} + \cdots + 10^{n+rem-1}) = 10^{n+rem} - 10^n .

rem=0rem = 0 时该位取 bi=0b_i = 0,代价为 00,即让悬挂进位停下。

k=0k = 0 可以单独用 O(n)O(n) 处理:此时任何一位都不能产生进位,所以最低位全部取 00,直到第一个满足 ai⩽8a_i \leqslant 8 的位置 ii 取 bi=10−aib_i = 10 - a_i(ai=0a_i = 0 时取 00),答案是 10i10^i;若 xx 全是 99,则取 i=ni = n,答案是 10n10^n。例如 x=9x = 9 时答案是 1010,x=12345678x = 12345678 时答案是 11。

用小数据检验一下状态含义。取 x=19x = 19,则 a=[9,1]a = [9, 1],k=2k = 2:

处理完 dp[0][0]dp[0][0] dp[0][1]dp[0][1] dp[0][2]dp[0][2] dp[1][0]dp[1][0] dp[1][1]dp[1][1] dp[1][2]dp[1][2]
初始 00 – – – – –
i=0, a0=9i=0,\ a_0=9 00 – – – 11 –
i=1, a1=1i=1,\ a_1=1 00 11 – – 9090 8181

表中 -- 表示不可达。逐格读法:

  • i=0i=0:个位取 b0=0b_0 = 0 得到 dp[0][0]=0dp[0][0] = 0;a0=9⩾1a_0 = 9 \geqslant 1,取 b0=10−9=1b_0 = 10 - 9 = 1 新开一次进位,得到 dp[1][1]=1dp[1][1] = 1,对应 yy 的个位是 11。
  • i=1i=1:从 dp[0][0]dp[0][0] 不进位仍是 00;从 dp[1][1]dp[1][1] 结束进位(a1=1⩽8a_1 = 1 \leqslant 8,b1=0b_1 = 0)得到 dp[0][1]=1dp[0][1] = 1;从 dp[0][0]dp[0][0] 在十位新开进位得 10−1=910 - 1 = 9,代价 9×10=909 \times 10 = 90,得到 dp[1][1]=90dp[1][1] = 90;从 dp[1][1]dp[1][1] 继续进位得 9−1=89 - 1 = 8,代价 1+8×10=811 + 8 \times 10 = 81,得到 dp[1][2]=81dp[1][2] = 81。

结尾 dp[0][2]dp[0][2] 不可达,只需比较两个进位未停的状态:

  • j=1j = 1,还差 rem=1rem = 1 次进位,90+(103−102)=99090 + (10^3 - 10^2) = 990。此时 y=990y = 990,19+990=100919 + 990 = 1009,进位发生在十位和百位,恰好两次,合法。
  • j=2j = 2,rem=0rem = 0,81+(102−102)=8181 + (10^2 - 10^2) = 81。此时 y=81y = 81,19+81=10019 + 81 = 100,两次进位,同样合法。

两者都合法,取最小值 8181。这个例子也说明尾部公式不能漏:y=990y = 990 的进位是越过 xx 的最高位后在更高位补足出来的。

答案最大时约有 3535 位十进制数(x=1017, k=18x = 10^{17},\ k = 18),超过 long long,所以实现里用 unsigned __int128 保存状态值,并手写十进制输出。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-03 12:31
 * update_at: 2026-10-03 12:31
 */
// P9718 [EC Final 2022] Best Carry Player 2
// 求最小的正整数 y,使 x + y 的竖式加法中「进位次数」恰好为 k,无解输出 -1。
//
// 核心模型:把 x 从低位到高位写成 a[0..n-1],y 的同位数字记作 b[i]。
// 设 c[i] 表示第 i 位向第 i+1 位的进位(c[0]=0),则
//   c[i+1] = 1 等价于 a[i] + b[i] + c[i] >= 10。
// 于是「进位次数」= 进位集合 { i : c[i+1]=1 } 的大小。
//
// 一个位置要想「新产生」进位,必须 a[i] + c[i] + b[i] >= 10,即
//   - c[i]=0 时需要 a[i] >= 1,最小 b[i] = 10 - a[i](把和顶到 10);
//   - c[i]=1 时需要 b[i] >= 9 - a[i],最小 b[i] = 9 - a[i]。
// 注意 c[i]=0 且 a[i]=0 时即使 b[i]=9 也只能得到 9,无法产生进位,
// 所以每个进位段的首位必须满足 a[i] >= 1。
// 同理,「进位到此为止」需要 a[i] + b[i] + c[i] <= 9:
//   - c[i]=1 时最小代价 b[i]=0,且要求 a[i] <= 8(a[i]=9 时无法停下)。
//
// 于是问题变成:在数位上选出恰好 k 个进位位置,使 y 的值最小。
// 用 dp[c][j] = 处理完前 i 位后、当前进位状态为 c、已产生 j 次进位时 y 的最小值,
// 从低位向高位转移即可;每一位的代价 b[i] 会乘以 10^i。
// 超出 x 长度的位置 a[i]=0,此时:
//   - c=0 什么也不做,代价 0;
//   - c=1 一路继续 j' 位,最小 y 是 1 后跟 (n + j') 个 0(即 10^(n+j'))。
// 答案可能达到 10^36 量级,用 unsigned __int128 存下绰绰有余。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;

const int MAXN = 20;      // x < 10^18,位数不超过 18
const int MAXK = 20;      // k <= 18
const int MAXP = 45;      // 10^36 < 2^128,够用

unsigned __int128 pow10u[MAXP];              // pow10u[i] = 10^i
unsigned __int128 dp[2][MAXK];               // dp[进位状态][已用进位次数]
unsigned __int128 ndp[2][MAXK];
int a[MAXN + 1];                             // x 从低到高的各位数字
int n;                                       // x 的位数

const unsigned __int128 U128_INF = ~(unsigned __int128)0;

// unsigned __int128 没有现成的输出,手动转十进制
void print_u128(unsigned __int128 v) {
    char s[64];
    int len = 0;
    if (v == 0) {
        putchar('0');
        return;
    }
    while (v > 0) {
        s[len++] = char('0' + (int)(v % 10));
        v /= 10;
    }
    while (len > 0) {
        putchar(s[--len]);
    }
}

// 拆出 x 的每一位,a[0] 是最低位
void split_digits(unsigned long long x) {
    n = 0;
    while (x > 0) {
        a[n++] = (int)(x % 10);
        x /= 10;
    }
    if (n == 0) {
        a[n++] = 0;
    }
}

void solve_one(unsigned long long x, int k) {
    split_digits(x);

    // k = 0 的情况:y 的每一位都不能和 x 产生进位
    // 只要某一位 a[i] <= 8,该位取 b[i] = 10 - a[i](a[i]=0 时取 0)即可,
    // 更低的位全部取 0,所以答案是 10^i(i 是最小的满足 a[i] <= 8 的下标)。
    // 若 x 全是 9,则取 y = 10^n,此时 a[i]=0(i=n)满足条件。
    if (k == 0) {
        int i = 0;
        while (i < n && a[i] == 9) {
            i++;
        }
        print_u128(pow10u[i]);
        putchar('\n');
        return;
    }

    // 初始:还没处理任何位,进位状态 0,已用 0 次进位,y 的值 0
    for (int c = 0; c < 2; c++) {
        for (int j = 0; j <= k; j++) {
            dp[c][j] = U128_INF;
        }
    }
    dp[0][0] = 0;

    for (int i = 0; i < n; i++) {
        for (int c = 0; c < 2; c++) {
            for (int j = 0; j <= k; j++) {
                ndp[c][j] = U128_INF;
            }
        }
        unsigned __int128 w = pow10u[i];   // 第 i 位的权值
        for (int c = 0; c < 2; c++) {
            for (int j = 0; j <= k; j++) {
                if (dp[c][j] == U128_INF) {
                    continue;
                }
                if (c == 0) {
                    // 不产生进位:b[i] = 0
                    if (dp[c][j] < ndp[0][j]) {
                        ndp[0][j] = dp[c][j];
                    }
                    // 新产生一次进位:要求 a[i] >= 1,b[i] = 10 - a[i]
                    if (a[i] >= 1 && j + 1 <= k) {
                        unsigned __int128 val = dp[c][j] + (unsigned __int128)(10 - a[i]) * w;
                        if (val < ndp[1][j + 1]) {
                            ndp[1][j + 1] = val;
                        }
                    }
                } else {
                    // 进位结束:要求 a[i] <= 8,b[i] = 0
                    if (a[i] <= 8 && dp[c][j] < ndp[0][j]) {
                        ndp[0][j] = dp[c][j];
                    }
                    // 进位继续:b[i] = 9 - a[i],又产生一次进位
                    if (j + 1 <= k) {
                        unsigned __int128 val = dp[c][j] + (unsigned __int128)(9 - a[i]) * w;
                        if (val < ndp[1][j + 1]) {
                            ndp[1][j + 1] = val;
                        }
                    }
                }
            }
        }
        for (int c = 0; c < 2; c++) {
            for (int j = 0; j <= k; j++) {
                dp[c][j] = ndp[c][j];
            }
        }
    }

    unsigned __int128 ans = U128_INF;

    // 结尾一:不进位,必须恰好用满 k 次进位
    if (dp[0][k] < ans) {
        ans = dp[0][k];
    }

    // 结尾二:第 n-1 位产生了进位,即 c[n]=1。此时最高位之外 a[i]=0,
    // 补 b[i]=9 可以让进位继续:0 + 9 + 1 = 10,又记账一次进位。
    // 设还需要 rem = k - j 次进位,就要在第 n ~ n+rem-1 位都补 9,
    // 代价为 9 * (10^n + ... + 10^(n+rem-1)) = 10^(n+rem) - 10^n。
    // rem = 0 时代价为 0:该位取 b=0 即可让最终悬挂的进位停下。
    for (int j = 0; j <= k; j++) {
        if (dp[1][j] == U128_INF) {
            continue;
        }
        int rem = k - j;
        unsigned __int128 val = dp[1][j] + pow10u[n + rem] - pow10u[n];
        if (val < ans) {
            ans = val;
        }
    }

    if (ans == U128_INF) {
        puts("-1");
    } else {
        print_u128(ans);
        putchar('\n');
    }
}

int main() {
    pow10u[0] = 1;
    for (int i = 1; i < MAXP; i++) {
        pow10u[i] = pow10u[i - 1] * 10;
    }

    int T;
    scanf("%d", &T);
    while (T--) {
        unsigned long long x;
        int k;
        scanf("%llu %d", &x, &k);
        solve_one(x, k);
    }
    return 0;
}

复杂度

设 xx 的位数为 n⩽18n \leqslant 18。单组时间 O(nk)O(nk),k⩽18k \leqslant 18 时不超过 324324 次转移;T=105T = 10^5 时总量约 3×1073 \times 10^7。空间为常数级数组,O(1)O(1)。

总结

  • 本题的约束写在「进位次数」上,所以搜索对象应该是进位位置的形状,而不是 yy 的数值;
  • 一旦固定 aia_i 和进入的进位 cic_i,每位的最优 bib_i 唯一:不进位取 00,新开进位取 10−ai10 - a_i(要求 ai⩾1a_i \geqslant 1),进位继续取 9−ai9 - a_i,结束进位取 00(要求 ai⩽8a_i \leqslant 8);
  • 把「当前进位状态」和「已用进位次数」作为 DP 的两维,就能在 O(nk)O(nk) 内求出最小 yy;xx 之外的位用 10n+rem−10n10^{n+rem} - 10^n 公式一次性补齐;
  • 答案可达 103510^{35},C++ 里要想到 unsigned __int128(或手写高精度),这是本题最常见的失分点。