CF1070A 题解

CF原题

洛谷原题

AC记录

问题简述

求出最小的 \(n\), 使得 \(d|n\), 且 \(n\) 的每个数位上的和等于 \(s\)

\(1 \leq d \leq 500\)

\(1 \leq s \leq 5000\)

solution

我们考虑暴力枚举\(n\)的值。由于\(s\)最大可以取到\(5000\),假设我们每一个数位都填入\(9\)的情况下,那整个数的长度也到达了大约\(500\)位。每个位有\(10\)个数字可以填。那么总的可能的\(n\)数不小于\(10^{500}\)个, 这显然是不可接受的。

我们分析枚举\(n\)的过程,显然,是有后效性的,后效性影响了我们对于\(d\)整除的判断,和数位和大小的判断。

显然,一次就构造出可以被\(d\)整除的数太难了,不如说构造出\(\pmod d\)\(0\)的数。然后在不同的余数之间相互转移。

我们研究以下后效性,设目前已经决定的数为\(bef\),我们要加入一个数\(num\),使得决定的数变成\(aft\)

显然,\(aft=bef \times 10 + num\)

我们发现,我们\(bef\)可以表示为\(kd+a\),其中$$k=\lfloor\frac{bef}{d}\rfloor, a \equiv bef \pmod d$$

显然,在从\(bef \times 10\)后,原先\(kd\)的部分仍然可以被\(d\)整除,那么核心在于\(a\)

然后我们可得

\[aft \equiv (a \times 10+num) \pmod d \]

那么关于整除限制的处理,转化为了对余数的记录和处理。我们就可以不用关心这个数本身。

那么,我们就可以使用两个变量来描述一个数对接下来选择的后效性。分别是模\(d\)的余数\(r\),和各个数位上的和\(sum\)

那么对于两个或更多已经决定的数,诺它们的\(r\)\(sum\)均相同,那么他们对于接下来选择的后效性是等价的。

同时,题目中要求了保留最小的\(n\),那么对于相同的\(r,sum\)我们显然只需要保留最小的数。

显然,在有意义的状态中,\(0 \leq r \leq d-1,0 \leq sum \leq s\)

那么,我们总共就需要计算并保留\(O(有意义的r \times 有意义的sum)=O(d \times s)\)个有意义的状态。每个状态至多保留一个最优的数。

那么我们总的枚举数量就从\(10^{500}\)减小到了\(d \times s=2.5 \times 10^6\) 个有意义的数。这显然是可以接受的。

我们考虑如何计算状态。

显然,我们要从个一个空的数转移到一个满足要求的数。即从\(\{r=0,sum=0\}\)转移到\(\{r=0,sum=s\}\)

首先,我们要指导这个状态是否已经有合法的数了。故需要bool tag[maxd][maxs];

同时,我们需要记录状态的数本身,所以需要string ans[maxd][maxs];

下面,我们称一个用于描述后效的二元组\((r,sum)\)为一个状态。用于转移计算其他状态的数的状态成为前驱状态,被转移的状态称为后继状态。

当然,大部分状态可以同时为前驱状态和后继状态。

我们复杂度与状态数同阶的重要的一点是,我们每个状态只被更新一次。

我们可以使用\(bfs\)队列和正确的入队顺序来保证一个状态第一个更新的数就是这个状态的最优的(最小的)数。

显然,我们是从数的长度为\(0\)开始往外扩展。所以位数小的数肯定先被处理。对于相同位数的,我们要让具体数位上数字较小的先被处理,那么我们在扩展数位的时候,就要先尝试\(0\),然后\(1,2\),最后到\(9\)

此时我们发现一个问题,是每次状态的转移的时候,我们都需要记录新的\(ans\)数组值,那么我们就需要将前驱的\(ans\)拷贝到后继的\(ans\),然后加上新的位数。 单次拷贝的复杂度是\(O(n)\)的,这使得时间复杂度要再乘上一个\(n\),这不可接受,同时,\(ans\)数组这也会导致\(MLE\).

我们可以每个状态之记录这个状态相较于前驱状态增加的字母,同时记录具体的前驱状态。然后从最终状态,一点点向初始状态跳。我们在跳的过程中可以得到每次增加的数字,组合起来就是最终状态的数了。

所以需要pair<ll,ll> in[maxd][maxs]char ch[maxd][maxs]分别记录每个状态增加的数字和前驱状态。

然后具体实现看代码吧:

#include <bits/stdc++.h>

using std::cin;
using std::cout;
using std::swap;
using std::max;
using std::min;
using std::vector;
using std::queue;
using std::fill;
using std::string;
using std::pair;

typedef long long ll;

const ll inf=1e18;
const ll maxd=505;
const ll maxs=5005;

struct node{
    ll r,sum;
};

queue<node> que;
ll tag[maxd][maxs]; //记录每个状态的数是否已经确定的
char ch[maxd][maxs]; //记录每个状态相较于前驱状态增加的数
pair<ll,ll> in[maxd][maxs]; //记录每个状态的前驱状态
vector<char> ans; //最终从最终状态往初始状态跳的时候,整理答案用的
ll d,s;

int main() {
    scanf("%lld%lld",&d,&s);

    que.push({0,0});
    tag[0][0]=1; //初始状态是余数0,位数和0

    while(que.size()) {
        node now=que.front();
        que.pop();
        for(ll i=0;i<10;++i) {
			//枚举加入的数
            if(i==0 && now.sum==0) {
                continue;
            }
            ll tmpr,tmps; //tmp_r   tmp_sum
			//更新余数和数位和
            tmpr=(now.r*10+i)%d;
            tmps=now.sum+i;
            if(tmps>s) {
				//数位和超了那么这个状态就没用的
                continue;
            }
            if(tag[tmpr][tmps]) {
				//已经有相同状态下更小的数出现了,这个数没用了
                continue;
            }
			//记录新加入的值,前驱状态,记录当前状态已确定具体数
            ch[tmpr][tmps]='0'+i;
            in[tmpr][tmps]={now.r,now.sum};
            tag[tmpr][tmps]=1;
            //loge[tmpr][tmps]=tmp;
            que.push(node{tmpr,tmps});
        }
    }

   //没答案
    if(!tag[0][s]) {
        puts("-1");
        return 0;
    }
 
 
   //有答案,从最终状态往初始状态跳
    pair<ll,ll> sta={0,s},tmp; //state
    do{
        //printf("%c",ch[0][s]);
        ans.push_back(ch[sta.first][sta.second]);
        sta=in[sta.first][sta.second];
    }while(sta.second!=0);

    for(ll i=ans.size()-1;i>=0;--i) {
        printf("%c",ans[i]);
    }
    puts("");
    return 0;
}

posted @ 2026-09-04 23:48  txp2025  阅读(23)  评论(0)    收藏  举报