AIGC标识 同余最短路相关题目

luogu3403 跳楼机

这题的核心是:按模 \(x\) 的余数分层,对每个余数求出“最小可达楼层”,然后利用可以无限次 \(+x\) 来统计该余数下所有不超过 \(h\) 的可达楼层。


1. 关键观察

跳楼机有四种操作:

  1. \(+x\)
  2. \(+y\)
  3. \(+z\)
  4. 回到第 \(1\) 层

其中“回到第 \(1\) 层”其实可以忽略。因为如果一条路径中回到了第 \(1\) 层,那么回到第 \(1\) 层之前的操作对最终到达的楼层没有贡献,删掉它们后仍然是一条从第 \(1\) 层出发、只向上走的合法路径。

所以问题转化为:

从第 \(1\) 层出发,每次可以向上走 \(x,y,z\) 层,问能到达多少个不超过 \(h\) 的楼层。


2. 为什么按模 \(x\) 分类

如果当前楼层是 \(u\),那么加一次 \(x\) 后变成 \(u+x\),它和 \(u\) 模 \(x\) 的余数相同。

因此对于每个余数 \(r \in [0,x-1]\),我们只关心:

到达余数为 \(r\) 的楼层中,最小楼层是多少?

设这个最小楼层为 \(d[r]\)。

那么所有形如:

\[d[r],\ d[r]+x,\ d[r]+2x,\dots \]

的楼层都可以到达,而且这些恰好是该余数下所有可到达的楼层。

如果 \(d[r] \le h\),那么该余数贡献的楼层数量为:

\[\left\lfloor \frac{h-d[r]}{x} \right\rfloor + 1 \]


3. 建图跑最短路

把余数 \(0,1,\dots,x-1\) 看成图上的点。

从当前余数 \(u\) 出发:

  • 加 \(y\) 层后,余数变成 \((u+y)\bmod x\),边权是 \(y\)
  • 加 \(z\) 层后,余数变成 \((u+z)\bmod x\),边权是 \(z\)

为什么没有加 \(x\) 的边?

因为加 \(x\) 后余数不变,是自环,而且边权为正,不会让某个余数的最小楼层变小。加 \(x\) 的贡献在最后统计答案时已经通过公式计算了。

起点是第 \(1\) 层,所以:

\[d[1\bmod x]=1 \]

然后跑 Dijkstra,求出每个余数的最小可达楼层。


4. 代码

#include <bits/stdc++.h>
#define IOI ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#define int long long
// #define int unsigned long long
#define ull unsigned long long
#define i128 __int128_t
// #define popcount(x) __builtin_popcountll(x)
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
#define pii pair<int,int>
#define pll pair<ull,ull>
using namespace std;
const int N=1e5+5,M=35;
const int mod=998244353;
const int inf=1.5e18;
mt19937_64 rng(time(0));
inline int rnd(int l,int r){return rng()%(r-l+1)+1;}
priority_queue<pii,vector<pii>,greater<pii>> q;
signed main()
{
    IOI
    int h,x,y,z;
    cin>>h>>x>>y>>z;
    vector<int> d(N,inf),vis(N,0);
    d[1%x]=1;
    q.push({1%x,1});
    while(!q.empty())
    {
        int i=q.top().se;
        q.pop();
        if(vis[i]) continue;
        vis[i]=1;
        if(d[i]+y<d[(i+y)%x])
        {
            d[(i+y)%x]=d[i]+y;
            q.push({d[(i+y)%x],(i+y)%x});
        }
        if(d[i]+z<d[(i+z)%x])
        {
            d[(i+z)%x]=d[i]+z;
            q.push({d[(i+z)%x],(i+z)%x});
        }
    }
    int ans=0;
    for(int i=0;i<x;i++) if(d[i]<=h) ans=ans+(h-d[i])/x+1;
    cout<<ans;
    return 0;
}

5. 代码逐段解释

int h,x,y,z;
cin>>h>>x>>y>>z;

读入楼层上限和三种步长。

vector<int> d(N,inf),vis(N,0);
d[1%x]=1;
q.push({1%x,1});

d[i] 表示到达余数 \(i\) 的最小楼层。

起点是第 \(1\) 层,所以余数是 \(1\bmod x\),最小楼层为 \(1\)。

注意这里更规范的写法应是:

q.push({1, 1 % x});

因为优先队列中 pair 的第一个元素应该是距离,第二个元素是节点。原代码在 \(x>1\) 时由于 \(1\bmod x=1\),恰好等价;在 \(x=1\) 时入队的是无效节点,但不影响 d[0] 的答案。建议改成规范写法。

while(!q.empty())
{
    int i=q.top().se;
    q.pop();
    if(vis[i]) continue;
    vis[i]=1;

Dijkstra 小根堆。第一次弹出某个节点时,它的最短路已经确定。

if(d[i]+y<d[(i+y)%x])
{
    d[(i+y)%x]=d[i]+y;
    q.push({d[(i+y)%x],(i+y)%x});
}

从余数 \(i\) 加 \(y\) 转移到新余数。

if(d[i]+z<d[(i+z)%x])
{
    d[(i+z)%x]=d[i]+z;
    q.push({d[(i+z)%x],(i+z)%x});
}

从余数 \(i\) 加 \(z\) 转移。

int ans=0;
for(int i=0;i<x;i++) 
    if(d[i]<=h) 
        ans=ans+(h-d[i])/x+1;

统计每个余数的贡献。

更严谨的写法应加上 vis[i]:

if(vis[i] && d[i] <= h)
    ans += (h - d[i]) / x + 1;

原因是 inf = 1.5e18,而题目中 \(h\) 最大可以到 \(2^{63}-1\approx 9.22\times 10^{18}\)。如果某个余数不可达,d[i] 仍为 inf,当 \(h>inf\) 时,原来的 d[i]<=h 可能把不可达余数也算进去。加上 vis[i] 最稳妥。


6. 总结

这题的关键套路是:

  1. 忽略“回到第一层”,只考虑向上走。
  2. 按模 \(x\) 的余数分类。
  3. 对每个余数求最小可达楼层 \(d[r]\)。
  4. 每个余数贡献:

\[\left\lfloor \frac{h-d[r]}{x} \right\rfloor+1 \]

  1. 用 Dijkstra 在余数图上求 \(d[r]\)。

本质上是:同余最短路 + 计数。

luogu2662 牛场围栏

题意转化

每根原始长度为 \(l_i\) 的木料,最多可以削短 \(M\) 米,并且只能削去整数米。
因此第 \(i\) 种木料可以变成:

\[l_i,\ l_i-1,\ l_i-2,\dots,\ \max(1,l_i-M) \]

所有木料都可以无限使用,把选中的木料长度相加就是围栏长度。

问题变成:

给定一个正整数集合 \(S\),其中每个数都可以使用任意多次,求不能表示出的最大整数。
如果不存在这样的最大整数,输出 -1。

这是经典的“同余最短路 / 无限背包最大不可表示数”问题。


同余最短路

从集合 \(S\) 中任选一个数作为模数 \(mod\)。
通常选最小的数,但选任意一个 \(S\) 中的数都可以。

设:

\[d[r]=\text{所有能表示出的数中,模 }mod\text{ 余 }r\text{ 的最小数} \]

显然:

\[d[0]=0 \]

对于每个可用的木料长度 \(a\in S\),可以从余数 \(r\) 转移到:

\[(r+a)\bmod mod \]

边权为 \(a\)。

因为边权都是正数,所以用 Dijkstra 求最短路。

跑完最短路后:

  • 如果存在某个余数 \(r\) 满足 \(d[r]=+\infty\),说明这个余数下的所有数都不能表示。
    此时不能表示的数有无限多个,最大值不存在,输出 -1。
  • 否则,对于余数 \(r\),所有大于等于 \(d[r]\) 且模 \(mod\) 余 \(r\) 的数都可以表示。
    因此该余数下最大的不能表示的数是:

\[d[r]-mod \]

最终答案:

\[\max_{0\le r<mod}(d[r]-mod) \]

如果这个最大值小于 \(0\),说明所有非负整数都能表示,也输出 -1。


代码思路解释

原代码中:

for(int i=1;i<=n;i++)
{
    cin>>x;
    for(int j=x;j>=max(1ll,x-m);j--) flag[j]=1;
}

flag[j]=1 表示长度 \(j\) 是可以通过削短得到的可用木料长度。

然后代码使用最后读入的 x 作为模数。
因为最后读入的 x 本身也是某一种原始木料长度,它一定在 flag 中,所以把它作为模数是合法的。
不过一般选最小的可用长度作为模数,复杂度会更优。

d[0]=0;
q.push({0,0});

同余最短路的起点。

while(!q.empty())
{
    int i=q.top().se;
    q.pop();
    if(vis[i]) continue;
    vis[i]=1;
    for(int j=1;j<=3000;j++)
    {
        if(!flag[j]) continue;
        if(d[(i+j)%x]>d[i]+j)
        {
            d[(i+j)%x]=d[i]+j;
            q.push({d[(i+j)%x],(i+j)%x});
        }
    }
}

枚举所有可用木料长度 \(j\),从余数 \(i\) 转移到:\((i+j)\bmod x\)

边权为 \(j\)。

int ans=-1;
for(int i=0;i<x;i++) if(d[i]>x) ans=max(ans,d[i]-x);
cout<<ans;

对每个余数计算:\(d[i]-x\)

并取最大值。


原代码的一个隐患

原代码最后:

for(int i=0;i<x;i++) if(d[i]>x) ans=max(ans,d[i]-x);

如果某个余数不可达,则:

d[i] = inf = 1e18

此时 d[i] > x 成立,会执行:

ans = max(ans, inf - x)

得到一个巨大的错误答案。

正确做法是:

  • 只要存在某个余数不可达,就直接输出 -1;
  • 否则再计算最大值。

另外,如果所有数都可以表示,最终 ans 会保持 -1,这也是正确的。


修正后的完整代码

#include<bits/stdc++.h>
#define IOI ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#define int long long
#define pii pair<int,int>
using namespace std;
const int N=3e3+5;
const int inf=1e18;

signed main()
{
    IOI
    int n,m,x;
    cin>>n>>m;
    vector<int> d(N,inf);
    vector<bool> flag(N,0),vis(N,0);
    int mod=inf;
    for(int i=1;i<=n;i++)
    {
        cin>>x;
        for(int j=x;j>=max(1ll,x-m);j--) flag[j]=1;
    }
    for(int j=1;j<=3000;j++)
    {
        if(flag[j])
        {
            mod=j;
            break;
        }
    }
    d[0]=0;
    priority_queue<pii,vector<pii>,greater<pii>> q;
    q.push({0,0});
    while(!q.empty())
    {
        int i=q.top().se;
        q.pop();
        if(vis[i]) continue;
        vis[i]=1;
        for(int j=1;j<=3000;j++)
        {
            if(!flag[j]) continue;
            int v=(i+j)%mod;
            if(d[v]>d[i]+j)
            {
                d[v]=d[i]+j;
                q.push({d[v],v});
            }
        }
    }
    int ans=-1;
    for(int i=0;i<mod;i++)
    {
        if(d[i]==inf)
        {
            cout<<-1;
            return 0;
        }
        ans=max(ans,d[i]-mod);
    }
    if(ans<0) ans=-1;
    cout<<ans;
    return 0;
}

总结

  • 将“削短”转化为可用长度集合 \(S\)。
  • 使用同余最短路求每个余数的最小可表示数。
  • 答案:

\[\max_{r}(d[r]-mod) \]

  • 如果存在不可达余数,输出 -1。
  • 如果最终最大值为负数,也输出 -1。

[luoguP2371]BZOJ2118墨墨的等式

题意

给定 \(n\) 个非负整数 \(a_1,a_2,\dots,a_n\),以及区间 \([l,r]\),求有多少个 \(b\in[l,r]\) 使得方程

\[\sum_{i=1}^{n}a_ix_i=b \]

存在非负整数解 \(x_1,x_2,\dots,x_n\)。

等价于问:区间 \([l,r]\) 中有多少个数能被 \(a_1,a_2,\dots,a_n\) 非负线性表出。


关键观察

如果某个数 \(b\) 能被表出,那么 \(b+a_i\) 也能被表出(多用一个 \(a_i\) 即可)。

因此所有能被表出的数,在模某个 \(a_i\) 的余数下构成一个“向上封闭”的集合:一旦某个余数 \(r\) 的最小可达值为 \(d[r]\),那么该余数下所有 \(\ge d[r]\) 的数都可以被表出。

这就是同余最短路的经典应用。


算法:同余最短路

1. 选择模数

任选一个非零的 \(a_i\) 作为模数 \(x\),设:

\[d[r]=\text{能被表出且模 }x\text{ 余 }r\text{ 的最小数} \]

显然 \(d[0]=0\)。

2. 建图

以余数 \(0,1,\dots,x-1\) 为节点。对每个 \(a_j\)(\(j\ne\) 模数对应的下标),从余数 \(i\) 连边到:

\[(i+a_j)\bmod x \]

边权为 \(a_j\)。

因为 \(d[r]\) 要求最小,边权为正,所以用 Dijkstra 求最短路。

3. 统计答案

对每个余数 \(r\):

  • 若 \(d[r]>\text{上界}\),则该余数对上界以下没有贡献。
  • 否则该余数下所有形如

\[d[r]+k\cdot x\quad(k\ge 0) \]

的数都能被表出。

于是 \([0,R]\) 内该余数的贡献为:

\[\left\lfloor\frac{R-d[r]}{x}\right\rfloor+1 \]

那么 \([l,r]\) 内的贡献就是:

\[\text{count}(r)-\text{count}(l-1) \]


代码逐段解释

读入与去零

int n,l,r;
cin>>n>>l>>r;
vector<int> a(n+1),d(N,inf);
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++) while(i<=n&&!a[i])
{
    n--;
    for(int j=i;j<=n;j++) a[j]=a[j+1];
}
if(n==0)
{
    cout<<0;
    return 0;
}

先读入 \(a_1,\dots,a_n\),再把所有 \(0\) 删掉。

为什么删 \(0\)?因为 \(a_i=0\) 对线性组合没有任何贡献,且不能作为模数(除以 \(0\) 无意义)。删完后若 \(n=0\),说明所有 \(a_i\) 都是 \(0\),唯一能表示的是 \(b=0\),而题目保证 \(l\ge 1\),所以答案为 \(0\)。

选模数

int x=a[1];
d[0]=0;

这里直接用 \(a_1\) 作为模数 \(x\)。

更优的做法是选最小的非零 \(a_i\) 作为模数,这样最短路图的点数最少,复杂度更低。原代码在 \(a_1\) 恰好最小时等价;否则只是复杂度常数偏大,但题目数据能过。

Dijkstra

priority_queue<pii,vector<pii>,greater<pii>> q;
q.push({0,0});
while(!q.empty())
{
    int i=q.top().se;
    q.pop();
    if(vis[i]) continue;
    vis[i]=1;
    for(int j=2;j<=n;j++) if(d[(i+a[j])%x]>d[i]+a[j])
    {
        d[(i+a[j])%x]=d[i]+a[j];
        q.push({d[(i+a[j])%x],(i+a[j])%x});
    }
}
  • 起点是余数 \(0\),最小值为 \(0\)。
  • 对每个其他 \(a_j\)(\(j\ge 2\)),从余数 \(i\) 转移到:

\[(i+a_j)\bmod x \]

  • 边权为 \(a_j\)。
  • 用 Dijkstra 求每个余数的最小可表示值。

注意这里从 \(j=2\) 开始,因为 \(a_1\) 是模数,加 \(a_1\) 等价于模 \(x\) 下不变,是自环,边权为正不会让 \(d\) 变小,无需考虑。

统计答案

int ans=0;
for(int i=0;i<x;i++)
{
    if(d[i]>r) continue;
    ans=ans+(r-d[i])/x+1;
    if(d[i]>l-1) continue;
    ans=ans-(l-1-d[i])/x-1;
}
cout<<ans;

对每个余数 \(i\):

  1. 若 \(d[i]>r\),这个余数在 \([0,r]\) 内没有贡献,跳过。
  2. 计算该余数在 \([0,r]\) 内的贡献:

\[\left\lfloor\frac{r-d[i]}{x}\right\rfloor+1 \]

  1. 若 \(d[i]>l-1\),说明在 \([0,l-1]\) 内该余数没有贡献,不减。
  2. 否则减去 \([0,l-1]\) 内的贡献:

\[\left\lfloor\frac{(l-1)-d[i]}{x}\right\rfloor+1 \]

等价写法就是原代码中的:

ans=ans-(l-1-d[i])/x-1;

完整 AC 代码(保持原框架)

#include <bits/stdc++.h>
#define IOI ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#define int long long
// #define int unsigned long long
#define ull unsigned long long
#define i128 __int128_t
// #define popcount(x) __builtin_popcountll(x)
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
#define pii pair<int,int>
#define pll pair<ull,ull>
#define CCF
#define che
#define dui
using namespace std;
const int N=5e5+5,M=35;
const int mod=998244353;
const int inf=1.5e18;
mt19937_64 rng(time(0));
inline int rnd(int l,int r){return rng()%(r-l+1)+1;}
signed main()
{
    IOI
    int n,l,r;
    cin>>n>>l>>r;
    vector<int> a(n+1),d(N,inf);
    for(int i=1;i<=n;i++) cin>>a[i]; 
    for(int i=1;i<=n;i++) while(i<=n&&!a[i])
    {
        n--;
        for(int j=i;j<=n;j++) a[j]=a[j+1];
    }
    if(n==0)
    {
        cout<<0;
        return 0;
    }
    vector<bool> vis(N,0);
    int x=a[1];
    d[0]=0;
    priority_queue<pii,vector<pii>,greater<pii>> q;
    q.push({0,0});
    while(!q.empty())
    {
        int i=q.top().se;
        q.pop();
        if(vis[i]) continue;
        vis[i]=1;
        for(int j=2;j<=n;j++) if(d[(i+a[j])%x]>d[i]+a[j])
        {
            d[(i+a[j])%x]=d[i]+a[j];
            q.push({d[(i+a[j])%x],(i+a[j])%x});
        }
    }
    int ans=0;
    for(int i=0;i<x;i++)
    {
        if(d[i]>r) continue;
        ans=ans+(r-d[i])/x+1;
        if(d[i]>l-1) continue;
        ans=ans-(l-1-d[i])/x-1;
    }
    cout<<ans;
    return 0;
}

arc084 D - Small Multiple

题目

给定正整数 \(K\),求一个 \(K\) 的正整数倍 \(S\),使得 \(S\) 的十进制数位和最小,输出这个最小数位和。

例如 \(K=6\) 时,\(12\) 是 \(6\) 的倍数,数位和为 \(1+2=3\),且不存在数位和更小的正整数倍,所以答案为 \(3\)。


核心思路:逐位构造十进制数

任何一个正整数都可以写成十进制数,并且可以从最高位开始,每次在末尾添加一位数字来构造。

假设当前构造出的数模 \(K\) 的余数是 \(r\),数位和为 \(s\)。
如果在末尾添加数字 \(j\in[0,9]\),新数为:

\[N' = N\cdot 10+j \]

新余数为:

\[r' = (r\cdot 10+j)\bmod K \]

新的数位和为:

\[s' = s+j \]

于是问题转化为一个图上的最短路问题:

  • 节点:余数 \(0,1,\dots,K-1\)
  • 从余数 \(r\) 出发,枚举数字 \(j\in[0,9]\),连边到 \((r\cdot10+j)\bmod K\),边权为 \(j\)
  • 求从某个非零起点到余数 \(0\) 的最短路径长度

最短路径长度就是最小数位和。


状态定义

代码中:

int d[N],vis[N];
  • \(d[r]\):构造出的数模 \(K\) 余 \(r\) 时,能够得到的最小十进制数位和。
  • \(vis[r]\):SPFA 中表示节点 \(r\) 是否在队列中。

目标就是求 \(d[0]\)。


初始化:为什么从 \(1\) 到 \(9\) 开始

memset(d,0x3f,sizeof(d));
queue<int> q;
for(int i=1;i<=9;i++)
{
    d[i%k]=min(d[i%k],i);
    q.push(i%k);
}

最高位不能是 \(0\),否则会产生前导零,甚至构造出数字 \(0\)。
而题目要求的是 \(K\) 的正整数倍,所以不能把 \(0\) 当作起点。

因此初始时,把数字 \(1,2,\dots,9\) 作为最高位:

  • 数字 \(i\) 的余数是 \(i\bmod K\)
  • 数位和是 \(i\)

如果多个 \(i\) 映射到同一个余数,取数位和更小的那个。例如 \(K=3\) 时,\(1,4,7\) 都余 \(1\),显然保留 \(1\) 更优。


转移:在末尾添加一位数字

for(int j=0;j<10;j++) if(d[(i*10+j)%k]>d[i]+j)
{
    d[(i*10+j)%k]=d[i]+j;
    if(vis[(i*10+j)%k]==0)
    {
        vis[(i*10+j)%k]=1;
        q.push((i*10+j)%k);
    }
}

当前余数为 \(i\),数位和为 \(d[i]\)。
在末尾添加数字 \(j\) 后:

\[i' = (i\cdot10+j)\bmod K \]

数位和增加 \(j\),所以:

\[d[i'] = \min(d[i'], d[i]+j) \]

如果更新成功,就把新余数加入队列。

注意 \(j=0\) 时边权为 \(0\),表示在末尾加 \(0\),数位和不变。例如:

  • 从数字 \(1\) 加 \(0\) 得到 \(10\),数位和仍然是 \(1\)。
  • 从余数 \(r\) 转移到 \((r\cdot10)\bmod K\),代价为 \(0\)。

因此不能简单用普通 BFS 求最少步数,而要用能处理 \(0\) 边权的 SPFA 或 Dijkstra。


为什么 \(d[0]\) 就是答案

任意一个 \(K\) 的正整数倍 \(S\),都可以从它的最高位开始,逐位添加数字构造出来。

这条路径的边权之和,恰好等于 \(S\) 的十进制数位和。
所以 \(d[S\bmod K]=d[0]\) 一定不会大于 \(S\) 的数位和。

反过来,图上任意一条从 \(1\sim9\) 出发到余数 \(0\) 的路径,都对应一个非零的十进制数,并且这个数模 \(K\) 余 \(0\),即它是 \(K\) 的正整数倍。
路径长度等于该数的数位和。

因此:

\[d[0] = \min\{\text{数位和}(S)\mid S>0,\ S\equiv0\pmod K\} \]

正是题目所求。


代码中的 SPFA 细节

while(!q.empty())
{
    int i=q.front();
    q.pop();
    vis[i]=0;
    ...
}
  • 出队时把 vis[i] 置为 \(0\),表示它已经不在队列中。
  • 之后如果 \(d[i]\) 被更小的值更新,可以再次入队。
  • 这是 SPFA 的标准写法。

需要注意:初始化时 q.push(i%k) 并没有设置 vis[i%k]=1。
严格来说,初始化入队时也应该标记 vis。原代码没有标记,仍能 AC,因为最多导致初始节点重复入队,不影响最短路正确性,只是可能增加常数时间。

更规范的初始化可以写成:

for(int i=1;i<=9;i++)
{
    int r=i%k;
    if(i<d[r])
    {
        d[r]=i;
        if(!vis[r])
        {
            vis[r]=1;
            q.push(r);
        }
    }
}

关键点总结

  1. 把数位和转化为路径长度:在十进制末尾添数字 \(j\),数位和增加 \(j\)。
  2. 状态是模 \(K\) 的余数:只关心当前数模 \(K\) 的余数。
  3. 初始从 \(1\sim9\) 开始:保证最高位非零,避免构造出 \(0\)。
  4. 转移:

\[r'=(r\cdot10+j)\bmod K,\quad d[r']=\min(d[r'],d[r]+j) \]

  1. 答案是 \(d[0]\):余数为 \(0\) 表示是 \(K\) 的倍数,最短路径长度就是最小数位和。
  2. \(j=0\) 的边权为 \(0\):所以用 SPFA 或 Dijkstra,而不是普通 BFS。

完整代码(带注释)

#include <bits/stdc++.h>
#define IOI ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#define int long long
// #define int unsigned long long
#define ull unsigned long long
#define i128 __int128_t
// #define popcount(x) __builtin_popcountll(x)
#define fi first
#define se second
#define be begin()
#define en end()
#define pii pair<int,int>
#define pll pair<ull,ull>
#define CCF
#define che
#define dui
using namespace std;
const int N=1e5+5,M=35;
const int mod=998244353;
const int inf=1.5e18;
inline int lowbit(int x){return x&(-x);}
inline int popcount(int x){int cut=0;for(;x;x-=lowbit(x))++cut;return cut;}
mt19937_64 rng(time(0));
inline int rnd(int l,int r){return rng()%(r-l+1)+1;}
int d[N],vis[N];
signed main()
{
    IOI
    int k;
    cin>>k;
    memset(d,0x3f,sizeof(d));
    queue<int> q;
    for(int i=1;i<=9;i++)
    {
        d[i%k]=min(d[i%k],i);
        q.push(i%k);
    }
    while(!q.empty())
    {
        int i=q.front();
        q.pop();
        vis[i]=0;
        for(int j=0;j<10;j++) if(d[(i*10+j)%k]>d[i]+j)
        {
            d[(i*10+j)%k]=d[i]+j;
            if(vis[(i*10+j)%k]==0)
            {
                vis[(i*10+j)%k]=1;
                q.push((i*10+j)%k);
            }
        }
    }
    cout<<d[0];
    return 0;
}

abc443 F - Non-Increasing Number

一、题意转化

题目定义:将正整数 \(X\) 的十进制表示按个位、十位、百位……排列,得到的序列是不增的。

例如 \(126\):

  • 个位 \(6\)
  • 十位 \(2\)
  • 百位 \(1\)

得到序列 \((6,2,1)\),满足 \(6\ge 2\ge 1\),所以 \(126\) 是好整数。

换一个角度看,从最高位到最低位读,数位是非递减的:

\[1\le 2\le 6 \]

所以“好整数”就是:从最高位到最低位,数位单调不降的数。

例如:

  • \(112389\):\(1\le1\le2\le3\le8\le9\),是好整数。
  • \(443\):\(4\le4\) 但 \(4\not\le3\),不是好整数。
  • \(404\):\(4\not\le0\),不是好整数。

题目要求:找到最小的好整数,使得它是 \(N\) 的正整数倍。若不存在,输出 -1。


二、核心思路:在模 \(N\) 的余数上 BFS

一个好整数可以一位一位地构造。

假设当前已经构造出的数模 \(N\) 的余数为 \(m\),最后一位数字为 \(i\)。
因为好整数要求高位到低位非递减,所以下一位数字 \(j\) 必须满足:

\[j\ge i \]

添加下一位后,新数变成:

\[N'=N\cdot 10+j \]

新余数为:

\[m'=(m\cdot 10+j)\bmod N \]

新的最后一位就是 \(j\)。

因此可以用 BFS 搜索状态:

\[(m,i) \]

其中:

  • \(m\):当前数模 \(N\) 的余数,\(0\le m<N\);
  • \(i\):当前数的最后一位,\(0\le i\le9\)。

目标是找到某个状态 \((0,i)\),即当前数是 \(N\) 的倍数。


三、初始化:最高位不能为 \(0\)

最高位可以是 \(1,2,\dots,9\)。

对于最高位 \(i\):

  • 当前数就是 \(i\);
  • 余数为 \(i\bmod N\);
  • 最后一位为 \(i\)。

所以初始时把状态:

\[(i\bmod N,\ i) \]

加入队列,其中 \(i=1,2,\dots,9\)。

代码对应:

for(int i=1;i<=9;i++)
{
    if(f[i%n][i]==0)
    {
        q.push({i%n,i});
        f[i%n][i]=1;
        pre[i%n][i]={-1,-1};
    }
}

f[m][i] 表示状态 \((m,i)\) 是否已经访问过。
pre[m][i] 记录状态 \((m,i)\) 的前驱状态,用于最后还原整个数。


四、BFS 转移

从队首取出状态 \((m,i)\)。

如果 \(m=0\),说明当前数已经是 \(N\) 的倍数。
由于 BFS 按数位长度和字典序进行,第一次遇到 \(m=0\) 就是最小的答案,直接还原并输出。

否则枚举下一位 \(j\),要求:

\[j\in[i,9] \]

这样保证高位到低位非递减。

转移:

\[m'=(m\cdot 10+j)\bmod N \]

新状态为:

\[(m',j) \]

如果该状态未访问,则入队,并记录前驱:

\[pre[m'][j]=(m,i) \]

代码对应:

for(int j=i;j<=9;j++)
{
    int nxtm=(m*10+j)%n,nxti=j;
    if(f[nxtm][nxti]==0)
    {
        q.push({nxtm,nxti});
        f[nxtm][nxti]=1;
        pre[nxtm][nxti]={m,i};
    }
}

五、为什么 BFS 第一次找到的就是最小答案?

BFS 按层扩展:

  • 第 \(1\) 层:所有 \(1\) 位数 \(1\sim9\);
  • 第 \(2\) 层:所有 \(2\) 位好整数;
  • 第 \(3\) 层:所有 \(3\) 位好整数;
  • ……

数位越少,数值越小。
所以先找到的答案一定比位数更多的数小。

同一层内,初始入队顺序是 \(1,2,\dots,9\)。
每次扩展下一位时,\(j\) 从 \(i\) 到 \(9\) 依次枚举。
因此队列中同层状态按构造出的数的字典序排列,也就是按数值大小排列。

所以第一次遇到余数为 \(0\) 的状态,就是最小的好整数倍数。


六、路径还原

pre[m][i] 记录了到达状态 \((m,i)\) 的前一个状态。

从找到的终点 \((0,i)\) 开始,不断取出当前数字 \(i\) 压入栈,然后跳到前驱状态:

\[(m,i)\leftarrow pre[m][i] \]

直到前驱为 \((-1,-1)\),说明到达最高位。

因为是从最低位向最高位回溯,所以压栈顺序是低位到高位。
最后从栈顶依次弹出,就得到从最高位到最低位的完整数字。

代码:

inline void print(int m,int i)
{
    stack<int>t;
    while(1)
    {
        t.push(i);
        int nm=pre[m][i].fi;
        int ni=pre[m][i].se;
        if(nm==-1) break;
        m=nm;
        i=ni;
    }
    while(!t.empty())
    {
        cout<<t.top();
        t.pop();
    }       
}

七、代码逐段解释

int f[N][10],n;
pii pre[N][10];
queue<pii> q;
  • f[m][i]:状态 \((m,i)\) 是否访问过;
  • pre[m][i]:状态 \((m,i)\) 的前驱;
  • q:BFS 队列,存储状态 \((m,i)\)。
for(int i=1;i<=9;i++)
{
    if(f[i%n][i]==0)
    {
        q.push({i%n,i});
        f[i%n][i]=1;
        pre[i%n][i]={-1,-1};
    }
}

最高位取 \(1\sim9\),初始化状态。

while(!q.empty())
{
    int m=q.front().fi;
    int i=q.front().se;
    q.pop();

    if(m==0)
    {
        print(0,i);
        return 0;
    }

    for(int j=i;j<=9;j++)
    {
        int nxtm=(m*10+j)%n,nxti=j;
        if(f[nxtm][nxti]==0)
        {
            q.push({nxtm,nxti});
            f[nxtm][nxti]=1;
            pre[nxtm][nxti]={m,i};
        }
    }
}
cout<<-1;

BFS 主体:

  • 出队状态 \((m,i)\);
  • 若 \(m=0\),输出并结束;
  • 否则枚举下一位 \(j\in[i,9]\),转移并记录前驱;
  • 队列为空仍未找到,输出 -1。

八、总结

本题的关键是:

  1. 好整数等价于从高位到低位非递减。
  2. 在模 \(N\) 的余数上做 BFS,状态为 \((余数, 最后一位)\)。
  3. 最高位从 \(1\sim9\) 开始,后续每位 \(j\ge\) 上一位。
  4. BFS 第一次遇到余数 \(0\) 的状态就是最小答案。
  5. 用前驱数组还原整个数字,避免大整数溢出。

核心转移公式:

\[(m,i)\longrightarrow\left((m\cdot10+j)\bmod N,\ j\right),\quad j\ge i \]

目标状态:\(m=0\)

[luoguP9751](CSP-J 2023)旅游巴士

P9751 [CSP-J 2023] 旅游巴士 详解

一、题意理解

小 Z 从景区入口(1 号点)走到出口(n 号点)。

约束条件:

  1. 每条道路单向通行,通过恰好耗时 1 单位时间。
  2. 每条道路有开放时间 \(a_i\),只有时刻 \(\ge a_i\) 才能通过。
  3. 到达入口的时刻必须是 \(k\) 的非负整数倍(他乘坐入口巴士到达)。
  4. 到达出口的时刻也必须是 \(k\) 的非负整数倍(他乘坐出口巴士离开)。
  5. 他想一直沿道路移动,不想停留。

问题:求最早离开景区的时刻。若无法完成,输出 -1。


二、关键观察

1. 时间状态只需要记录模 \(k\) 的余数

因为最终要求到达出口时时间是 \(k\) 的倍数,所以时间的关键信息是它模 \(k\) 的余数。

设:

\[d[x][i] = \text{到达节点 }x\text{ 且时间模 }k\text{ 余 }i\text{ 的最早时刻} \]

目标是 \(d[n][0]\)。

2. 道路开放时间的处理

假设当前在节点 \(x\),时刻为 \(t\),想走边 \((x,y,a)\)。

  • 若 \(t\ge a\),可以直接通过,到达 \(y\) 的时刻为 \(t+1\)。
  • 若 \(t<a\),不能立即通过。

由于小 Z 不能“停留”,他需要消耗时间。代码的处理方式是:等到一个时刻 \(t'\),使得 \(t'\ge a\) 且 \(t'\equiv t\pmod{k}\),然后通过这条道路。

也就是说,等待的时间是 \(k\) 的整数倍,这样时间模 \(k\) 的余数保持不变。

这个等待逻辑对应代码:

if(nd<a) nd=nd+(a-nd+k-1)/k*k;
nd++;

第一行计算最小的 \(t'\ge a\) 且 \(t'\equiv nd\pmod{k}\):

\[t'=nd+\left\lceil\frac{a-nd}{k}\right\rceil\cdot k \]

第二行表示通过道路耗时 1,到达新节点时刻为 \(t'+1\)。

3. 为什么等待 \(k\) 的整数倍

等待 \(k\) 的整数倍时间,时间模 \(k\) 的余数不变。

这样,从入口出发时余数为 0,每走一条边余数加 1,最终到达出口时若余数回到 0,则时刻是 \(k\) 的倍数。

代码中状态转移为:

\[(x,i)\longrightarrow\left(y,(i+1)\bmod k\right) \]


三、算法:Dijkstra

把状态 \((x,i)\) 看成图中的节点,共 \(n\cdot k\) 个状态。

  • 起点:\((1,0)\),距离为 0。
  • 对每条从 \(x\) 出发的边 \((x,y,a)\),从状态 \((x,i)\) 转移到状态 \((y,(i+1)\bmod k)\)。
  • 转移代价不是固定值,而是根据当前时刻和开放时间计算出的“到达下一节点的时刻”。

因为要最小化到达时刻,且转移满足非负性,所以用 Dijkstra。


四、代码逐段解释

状态与优先队列

int n,m,k,d[N][M],vis[N][M];
vector<pii> e[N];
priority_queue<piii,vector<piii>,greater<piii>> q;
  • d[x][i]:到达节点 \(x\)、时间模 \(k\) 余 \(i\) 的最早时刻。
  • vis[x][i]:Dijkstra 的访问标记。
  • 优先队列元素为 (时刻, (节点, 余数))。

初始化

memset(d,0x3f,sizeof(d));
d[1][0]=0;
q.push({0,{1,0}});

在时刻 0 位于入口 1,余数为 0。

Dijkstra 主循环

while(!q.empty())
{
    int x=q.top().se.fi;
    int i=q.top().se.se;
    q.pop();
    if(vis[x][i]) continue;
    vis[x][i]=1;
    for(auto j:e[x])
    {
        int y=j.fi,a=j.se;
        int nd=d[x][i];
        if(nd<a) nd=nd+(a-nd+k-1)/k*k;
        nd++;
        if(nd<d[y][(i+1)%k])
        {
            d[y][(i+1)%k]=nd;
            q.push({nd,{y,(i+1)%k}});
        }
    }
}

对每条出边 \((x,y,a)\):

  1. 当前时刻 nd = d[x][i]。
  2. 若 nd < a,等到最小的 \(t'\ge a\) 且 \(t'\equiv nd\pmod{k}\)。
  3. 通过道路,时刻变为 nd+1。
  4. 到达节点 \(y\),余数变为 (i+1)%k。
  5. 若新时刻更优,更新并入队。

输出答案

if(d[n][0]>=0x3f3f3f3f) d[n][0]=-1;
cout<<d[n][0];

若 \(d[n][0]\) 仍为无穷大,说明无法到达,输出 -1。


五、总结

本题的核心是:

  1. 把“时间”按模 \(k\) 的余数分层,状态为 \((节点,时间\bmod k)\)。
  2. 走一条边时余数加 1。
  3. 若当前时刻小于道路开放时间,等到最小的 \(t'\ge a\) 且 \(t'\equiv t\pmod{k}\),保持余数不变。
  4. 用 Dijkstra 求 \(d[n][0]\),即到达出口且时刻为 \(k\) 的倍数的最早时刻。

转移公式:

\[t'=\begin{cases} t,&t\ge a\\ t+\left\lceil\dfrac{a-t}{k}\right\rceil\cdot k,&t<a \end{cases} \]

\[d[y][(i+1)\bmod k]=\min\left(d[y][(i+1)\bmod k],\ t'+1\right) \]

答案:

\[d[n][0] \]

若不可达,输出 -1。

CCPChenan2025G Plus Xor

一、题目大意

给定整数 \(a,b,c\),其中 \(b\) 固定不变。你可以对 \(a\) 任意次执行以下两种操作:

  1. \(a\leftarrow a+b\)
  2. \(a\leftarrow a\oplus b\)

问能否恰好得到 \(c\)。

多组测试,\(T\le 10^3\),\(0\le a,c\le 10^{18}\),\(1\le b\le 1000\),且保证 \(\sum b^2\le 10^6\)。


二、核心观察

1. 为什么只记录 \(a\bmod 1024\) 和 \(a\bmod b\)

操作 \(a\leftarrow a+b\) 会改变 \(a\bmod 1024\),但不改变 \(a\bmod b\)。

操作 \(a\leftarrow a\oplus b\) 比较特殊。因为 \(b\le 1000<1024=2^{10}\),所以 \(b\) 的二进制只占低 \(10\) 位。异或 \(b\) 只会改变 \(a\) 的低 \(10\) 位,高位保持不变。

因此 \(a\bmod 1024\) 就是 \(a\) 的低 \(10\) 位。只要知道了:

  • \(a\bmod 1024\)
  • \(a\bmod b\)

那么经过一次操作后,新的 \(a\) 的这两个余数就可以确定。

于是把状态定义为:

\[(\ a\bmod 1024,\ a\bmod b\ ) \]

状态数最多为:

\[1024\cdot b\le 1024\cdot 1000\approx 10^6 \]

在总限制 \(\sum b^2\le 10^6\) 下可以接受。


2. 为什么每个状态只保留最小的 \(a\)

对于同一个状态 \((x,y)\),可能有很多个可达的 \(a\) 值,它们都满足:

\[a\bmod 1024=x,\qquad a\bmod b=y \]

设其中两个值为 \(a_1<a_2\)。由于它们同状态,差值:

\[a_2-a_1 \]

同时是 \(1024\) 和 \(b\) 的倍数,因此是:

\[\operatorname{lcm}(1024,b) \]

的倍数。

而 \(b<1024\),所以异或 \(b\) 只影响低 \(10\) 位。因为 \(a_1,a_2\) 低 \(10\) 位相同,所以:

\[a_2\oplus b=(a_1\oplus b)+\bigl(a_2-a_1\bigr) \]

同理:

\[a_2+b=(a_1+b)+\bigl(a_2-a_1\bigr) \]

也就是说,从较大的 \(a_2\) 出发,操作后得到的结果,恰好比从较小的 \(a_1\) 出发得到的结果大同样的差值,并且新状态相同。

因此对于每个状态,只需要保留最小可达的 \(a\)。记:

\[d[x][y]=\text{在状态 }(x,y)\text{下能到达的最小 }a \]


3. 为什么判断 \(d[c\bmod 1024][c\bmod b]\le c\) 即可

如果存在可达的 \(a_0\) 满足:

\[a_0\le c,\qquad a_0\equiv c\pmod{1024},\qquad a_0\equiv c\pmod b \]

那么:

\[c-a_0 \]

同时是 \(1024\) 和 \(b\) 的倍数,所以是:

\[L=\operatorname{lcm}(1024,b) \]

的倍数。

而重复执行 \(\frac{L}{b}\) 次 \(a\leftarrow a+b\),总增加量恰好为 \(L\),并且 \(a\bmod 1024\) 与 \(a\bmod b\) 都回到原来的值。

因此可以从 \(a_0\) 出发,每次增加 \(b\),经过若干轮后恰好到达 \(c\)。

所以只要:

\[d[c\bmod 1024][c\bmod b]\le c \]

就说明 \(c\) 可达,输出 YES;否则输出 NO。


三、算法流程

  1. 读入 \(a,b,c\)。

  2. 清空当前 \(b\) 对应的状态数组。

  3. 初始状态:

    \[d[a\bmod 1024][a\bmod b]=a \]

    并把 \((a,a\bmod 1024,a\bmod b)\) 压入小根堆。

  4. 不断取出当前 \(a\) 最小的状态 \((x,y)\):

    • 执行 \(a\leftarrow a+b\),得到新状态和新值;
    • 执行 \(a\leftarrow a\oplus b\),得到新状态和新值;
    • 若新值更小,则更新并入堆。
  5. 最后检查:

    \[d[c\bmod 1024][c\bmod b]\le c \]

    成立输出 YES,否则输出 NO。


四、代码逐段解释

状态数组

int d[2010][2010];
priority_queue<piii,vector<piii>,greater<piii>> q;
  • d[x][y] 表示在状态 \((x,y)\) 下能到达的最小 \(a\)。
  • 优先队列按 \(a\) 从小到大排序。

初始化

for(int i=0;i<=1024;i++) for(int j=0;j<=b;j++) d[i][j]=inf;
d[a%1024][a%b]=a;
q.push({a,{a%1024,a%b}});

初始时只有 \(a\) 本身可达。


取出状态

int x=q.top().se.fi;
int y=q.top().se.se;
int num=q.top().fi;
q.pop();
if(d[x][y]!=num) continue;

num 就是当前状态对应的最小 \(a\)。如果它已经不是最新值,就跳过。


操作一:\(a\leftarrow a+b\)

nx=(d[x][y]+b)%1024;
ny=y;
nd=d[x][y]+b;
if(nd<d[nx][ny])
{
    d[nx][ny]=nd;
    q.push({nd,{nx,ny}});
}

因为加 \(b\) 后模 \(b\) 的余数不变,所以:

\[ny=y \]

新余数:

\[nx=(a+b)\bmod 1024 \]


操作二:\(a\leftarrow a\oplus b\)

int nx=x^b;
int ny=(d[x][y]^b)%b;
int nd=d[x][y]^b;
if(nd<d[nx][ny])
{
    d[nx][ny]=nd;
    q.push({nd,{nx,ny}});
}

新值:

\[a'=a\oplus b \]

新状态:

\[x'=a'\bmod 1024,\qquad y'=a'\bmod b \]


判断答案

if(d[c%1024][c%b]<=c) cout<<"YES"<<'\n';
else cout<<"NO"<<'\n';

只要 \(c\) 所在状态的最小可达值不超过 \(c\),就可以通过重复加 \(b\) 补齐差值到达 \(c\)。


五、总结

  • 状态压缩为 \((a\bmod 1024,\ a\bmod b)\)。
  • 每个状态只保留最小可达的 \(a\)。
  • 用优先队列按 \(a\) 从小到大扩展两种操作。
  • 最后检查 \(c\) 所在状态的最小可达值是否不超过 \(c\)。

核心判断:

\[d[c\bmod 1024][c\bmod b]\le c \]

成立则输出 YES,否则输出 NO。

abc482F - Mod M Divisor

题目

给定正整数 \(N,M\),对每个 \(k=0,1,\dots,M-1\),求最小的正整数 \(t\),使得 \(N^t\) 存在一个正因子 \(x\) 满足:

\[x\equiv k\pmod M \]

如果不存在这样的 \(t\),输出 -1。

数据范围:

  • \(1\le N\le 10^{14}\)
  • \(1\le M\le 4\times10^5\)

核心观察

1. 分解 \(N\) 的质因数

设:

\[N=\prod_{i=0}^{s-1}p_i^{\alpha_i} \]

其中 \(p_i\) 是不同质数,\(\alpha_i\ge1\)。

那么 \(N^t\) 的任意正因子都可以写成:

\[x=\prod_{i=0}^{s-1}p_i^{e_i} \]

其中:

\[0\le e_i\le t\alpha_i \]

也就是说,对于固定的 \(t\),第 \(i\) 个质因子的指数上界是 \(t\alpha_i\)。


2. 问题转化为“最小轮数”

对于一个因子 \(x\),设它使用的指数为 \(e_0,e_1,\dots,e_{s-1}\)。
它需要的最小 \(t\) 满足:

\[t\ge \left\lceil\frac{e_i}{\alpha_i}\right\rceil \]

对所有 \(i\) 都成立。因此:

\[t=\max_i\left\lceil\frac{e_i}{\alpha_i}\right\rceil \]

我们的目标是:对每个余数 \(k\),找到一组指数 \(e_i\),使得:

\[\prod_{i=0}^{s-1}p_i^{e_i}\equiv k\pmod M \]

并且让上面的最大值尽可能小。


3. 按“轮”扩展指数

可以这样理解:

  • 每一轮中,对于每个质因子 \(p_i\),可以选择给它增加 \(0,1,\dots,\alpha_i\) 个指数。
  • 完成 \(t\) 轮后,每个质因子的总指数上界正好是 \(t\alpha_i\)。
  • 所以 \(t\) 就是轮数。

于是可以按质因子顺序,逐轮构造因子,并用 0-1 BFS 求最小的轮数。


状态设计

定义:

\[d[v][i] \]

表示当前构造出的因子模 \(M\) 的余数为 \(v\),已经完成了若干轮,当前处理到第 \(i\) 个质因子时,所需的最小轮数。

这里的 \(i\) 表示:

  • \(i=s-1\):当前这一轮已经处理完所有质因子,下一步将开始新的一轮;
  • \(0\le i<s-1\):当前这一轮已经处理完第 \(0,1,\dots,i\) 个质因子,下一步处理第 \(i+1\) 个质因子。

初始状态:

\[d[1][s-1]=0 \]

因为因子 \(1\) 的余数为 \(1\),且还没有开始任何一轮,轮数为 \(0\)。


转移

情况一:\(i=s-1\)

当前一轮已经结束,需要开始新的一轮,轮数加 \(1\)。

然后处理第 \(0\) 个质因子,即选择给它增加 \(j\in[0,\alpha_0]\) 个指数:

\[v' = v\cdot p_0^j \bmod M \]

转移:

\[d[v'][0]=\min(d[v'][0],d[v][s-1]+1) \]

边权为 \(1\)。

情况二:\(i<s-1\)

当前这一轮还没有结束,不增加轮数。

处理下一个质因子,即第 \(i+1\) 个质因子,选择给它增加 \(j\in[0,\alpha_{i+1}]\) 个指数:

\[v' = v\cdot p_{i+1}^j \bmod M \]

转移:

\[d[v'][i+1]=\min(d[v'][i+1],d[v][i]) \]

边权为 \(0\)。


为什么用 0-1 BFS

转移的边权只有两种:

  • 开始新的一轮:边权 \(1\);
  • 同一轮内继续处理下一个质因子:边权 \(0\)。

因此可以用双端队列实现 0-1 BFS:

  • 边权为 \(0\) 时,压入队首;
  • 边权为 \(1\) 时,压入队尾。

这样每个状态第一次被取出时,就得到了最小轮数。


答案统计

对每个余数 \(k=0,1,\dots,M-1\):

\[\text{ans}_k=\min_{0\le i<s}d[k][i] \]

因为可能在某一轮中间就已经得到目标余数,不需要完成整轮。

特别地:

  • 若 \(\text{ans}_k=+\infty\),说明不可达,输出 -1。
  • 对于 \(k=1\),因子 \(1\) 总是可达的,且题目要求 \(t\) 为正整数,所以最小 \(t=1\),直接令答案为 \(1\)。

边界情况

\(M=1\)

所有数模 \(1\) 都为 \(0\),因子 \(1\) 就满足条件,最小 \(t=1\)。

输出:

1

\(N=1\)

此时:

\[N^t=1 \]

唯一正因子是 \(1\)。因此:

  • 当 \(M>1\) 时,只有 \(k=1\) 可达,答案为 \(1\);
  • 其他 \(k\) 输出 -1。

代码中已单独处理。


代码逐段详解

线性筛

for(int i=2;i<=M;i++)
{
    if(v[i]==0)
    {
        v[i]=i;
        pri.push_back(i);
    }
    for(auto x:pri)
    {
        if(x>v[i]||x>M/i) break;
        v[i*x]=x;
    }
}

筛出 \(2\sim 10^7\) 的质数,用于分解 \(N\)。
因为 \(N\le 10^{14}\),所以 \(\sqrt N\le 10^7\)。


质因数分解

for(auto x:pri)
{
    if(n%x) continue;
    o.push_back({x,0});
    while(n%x==0)
    {
        n=n/x;
        o.back().se++;
    }
}
if(n!=1) o.push_back({n,1});

把 \(N\) 分解成:

\[N=\prod p_i^{\alpha_i} \]

存储在 o 中,o[i].fi 是 \(p_i\),o[i].se 是 \(\alpha_i\)。

如果最后 \(n\ne1\),说明剩下一个大于 \(10^7\) 的质因子,其指数只能是 \(1\)。


初始化

int siz=o.size();
q.push_back({1,siz-1});
d[1][siz-1]=0;

因子 \(1\),余数 \(1\),轮数 \(0\),状态为“一轮结束”。


0-1 BFS

while(q.size())
{
    int v=q.front().fi,i=q.front().se;
    q.pop_front();

    if(i==siz-1)
    {
        int nv=v,ni=0,nd=d[v][i]+1;
        for(int j=0;j<=o[0].se;j++)
        {
            if(d[nv][ni]>nd)
            {
                d[nv][ni]=nd;
                q.push_back({nv,0});
            }
            nv=(i128)nv*o[0].fi%m;
        }
    }
    else
    {
        int nv=v,ni=i+1,nd=d[v][i];
        for(int j=0;j<=o[ni].se;j++)
        {
            if(d[nv][ni]>nd)
            {
                d[nv][ni]=nd;
                q.push_front({nv,ni});
            }
            nv=(i128)nv*o[ni].fi%m;
        }
    }
}
  • i==siz-1:开始新的一轮,轮数加 \(1\),处理第 \(0\) 个质因子,边权为 \(1\),压入队尾。
  • 否则:同一轮内继续处理下一个质因子,边权为 \(0\),压入队首。

内层循环枚举给当前质因子增加 \(0,1,\dots,\alpha\) 个指数,并同步更新余数。


输出答案

for(int i=0;i<m;i++)
{
    int ans=inf;
    for(int j=0;j<siz;j++) ans=min(ans,d[i][j]);
    if(ans==inf) ans=-1;
    if(i==1) ans=1;
    cout<<ans<<'\n';
}

对每个余数 \(k=i\),取所有状态中的最小轮数。
若不可达,输出 -1。
余数 \(1\) 直接输出 \(1\)。


完整代码

#include <bits/stdc++.h>
#define IOI ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#define int long long
#define ull unsigned long long
#define i128 __int128_t
#define COUT cout<<"\n------------------------------------------------------------------------------------------\n";
#define fi first
#define se second
#define be begin()
#define en end()
#define pii pair<int,int>
#define piii pair<int,pii>
#define pll pair<ull,ull>
#define CCF
#define che
#define dui
using namespace std;
const int N=5e5+5,M=1e7+5;
const int mod=998244353;
const int inf=1.5e18;
inline int lowbit(int x){return x&(-x);}
inline int popcount(int x){int cut=0;for(;x;x-=lowbit(x))++cut;return cut;}
mt19937_64 rng(time(0));
inline int rnd(int l,int r){int cnt=rng()%r+1;return cnt<l?cnt+l:cnt;}
inline int rnd(int len){return rng()%len+1;}
int n,m,v[M+5],d[N][30];
vector<int> pri;
vector<pii> o;
deque<pii> q;
signed main()
{
    IOI
    int n,m;
    cin>>n>>m;
    if(m==1){cout<<1;return 0;}
    if(n==1)
    {
        for(int i=0;i<m;i++)
        {
            if(i==1) cout<<"1\n";
            else cout<<"-1\n";
        }
        return 0;
    }
    for(int i=2;i<=M;i++)
    {
        if(v[i]==0)
        {
            v[i]=i;
            pri.push_back(i);
        }
        for(auto x:pri)
        {
            if(x>v[i]||x>M/i) break;
            v[i*x]=x;
        }
    }
    for(auto x:pri)
    {
        if(n%x) continue;
        o.push_back({x,0});
        while(n%x==0)
        {
            n=n/x;
            o.back().se++;
        }
    }
    if(n!=1) o.push_back({n,1});
    memset(d,0x3f,sizeof(d));
    int siz=o.size();
    q.push_back({1,siz-1});
    d[1][siz-1]=0;
    while(q.size())
    {
        int v=q.front().fi,i=q.front().se;
        q.pop_front();
        if(i==siz-1)
        {
             int nv=v,ni=0,nd=d[v][i]+1;
             for(int j=0;j<=o[0].se;j++)
             {
                 if(d[nv][ni]>nd)
                 {
                     d[nv][ni]=nd;
                     q.push_back({nv,0});
                 }
                 nv=(i128)nv*o[0].fi%m;
             }
        }
        else
        {
            int nv=v,ni=i+1,nd=d[v][i];
            for(int j=0;j<=o[ni].se;j++)
            {
                if(d[nv][ni]>nd)
                {
                    d[nv][ni]=nd;
                    q.push_front({nv,ni});
                }
                nv=(i128)nv*o[ni].fi%m;
            }
        }
    }
    for(int i=0;i<m;i++)
    {
        int ans=inf;
        for(int j=0;j<siz;j++) ans=min(ans,d[i][j]);
        if(ans==inf) ans=-1;
        if(i==1) ans=1;
        cout<<ans<<'\n';
    }
    return 0;
}
posted @ 2026-11-02 20:31  Aries2011  阅读(5)  评论(0)    收藏  举报