同余最短路相关题目
- luogu3403 跳楼机
- luogu2662 牛场围栏
- [luoguP2371]BZOJ2118墨墨的等式
- arc084 D - Small Multiple
- abc443 F - Non-Increasing Number
- [luoguP9751](CSP-J 2023)旅游巴士
- P9751 [CSP-J 2023] 旅游巴士 详解
- CCPChenan2025G Plus Xor
- abc482F - Mod M Divisor
luogu3403 跳楼机
这题的核心是:按模 \(x\) 的余数分层,对每个余数求出“最小可达楼层”,然后利用可以无限次 \(+x\) 来统计该余数下所有不超过 \(h\) 的可达楼层。
1. 关键观察
跳楼机有四种操作:
- \(+x\)
- \(+y\)
- \(+z\)
- 回到第 \(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] \le h\),那么该余数贡献的楼层数量为:
3. 建图跑最短路
把余数 \(0,1,\dots,x-1\) 看成图上的点。
从当前余数 \(u\) 出发:
- 加 \(y\) 层后,余数变成 \((u+y)\bmod x\),边权是 \(y\)
- 加 \(z\) 层后,余数变成 \((u+z)\bmod x\),边权是 \(z\)
为什么没有加 \(x\) 的边?
因为加 \(x\) 后余数不变,是自环,而且边权为正,不会让某个余数的最小楼层变小。加 \(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. 总结
这题的关键套路是:
- 忽略“回到第一层”,只考虑向上走。
- 按模 \(x\) 的余数分类。
- 对每个余数求最小可达楼层 \(d[r]\)。
- 每个余数贡献:
- 用 Dijkstra 在余数图上求 \(d[r]\)。
本质上是:同余最短路 + 计数。
luogu2662 牛场围栏
题意转化
每根原始长度为 \(l_i\) 的木料,最多可以削短 \(M\) 米,并且只能削去整数米。
因此第 \(i\) 种木料可以变成:
所有木料都可以无限使用,把选中的木料长度相加就是围栏长度。
问题变成:
给定一个正整数集合 \(S\),其中每个数都可以使用任意多次,求不能表示出的最大整数。
如果不存在这样的最大整数,输出-1。
这是经典的“同余最短路 / 无限背包最大不可表示数”问题。
同余最短路
从集合 \(S\) 中任选一个数作为模数 \(mod\)。
通常选最小的数,但选任意一个 \(S\) 中的数都可以。
设:
显然:
对于每个可用的木料长度 \(a\in S\),可以从余数 \(r\) 转移到:
边权为 \(a\)。
因为边权都是正数,所以用 Dijkstra 求最短路。
跑完最短路后:
- 如果存在某个余数 \(r\) 满足 \(d[r]=+\infty\),说明这个余数下的所有数都不能表示。
此时不能表示的数有无限多个,最大值不存在,输出-1。 - 否则,对于余数 \(r\),所有大于等于 \(d[r]\) 且模 \(mod\) 余 \(r\) 的数都可以表示。
因此该余数下最大的不能表示的数是:
最终答案:
如果这个最大值小于 \(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\)。
- 使用同余最短路求每个余数的最小可表示数。
- 答案:
- 如果存在不可达余数,输出
-1。 - 如果最终最大值为负数,也输出
-1。
[luoguP2371]BZOJ2118墨墨的等式
题意
给定 \(n\) 个非负整数 \(a_1,a_2,\dots,a_n\),以及区间 \([l,r]\),求有多少个 \(b\in[l,r]\) 使得方程
存在非负整数解 \(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[0]=0\)。
2. 建图
以余数 \(0,1,\dots,x-1\) 为节点。对每个 \(a_j\)(\(j\ne\) 模数对应的下标),从余数 \(i\) 连边到:
边权为 \(a_j\)。
因为 \(d[r]\) 要求最小,边权为正,所以用 Dijkstra 求最短路。
3. 统计答案
对每个余数 \(r\):
- 若 \(d[r]>\text{上界}\),则该余数对上界以下没有贡献。
- 否则该余数下所有形如
的数都能被表出。
于是 \([0,R]\) 内该余数的贡献为:
那么 \([l,r]\) 内的贡献就是:
代码逐段解释
读入与去零
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\) 转移到:
- 边权为 \(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\):
- 若 \(d[i]>r\),这个余数在 \([0,r]\) 内没有贡献,跳过。
- 计算该余数在 \([0,r]\) 内的贡献:
- 若 \(d[i]>l-1\),说明在 \([0,l-1]\) 内该余数没有贡献,不减。
- 否则减去 \([0,l-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]\),新数为:
新余数为:
新的数位和为:
于是问题转化为一个图上的最短路问题:
- 节点:余数 \(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\) 后:
数位和增加 \(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\) 的正整数倍。
路径长度等于该数的数位和。
因此:
正是题目所求。
代码中的 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);
}
}
}
关键点总结
- 把数位和转化为路径长度:在十进制末尾添数字 \(j\),数位和增加 \(j\)。
- 状态是模 \(K\) 的余数:只关心当前数模 \(K\) 的余数。
- 初始从 \(1\sim9\) 开始:保证最高位非零,避免构造出 \(0\)。
- 转移:
- 答案是 \(d[0]\):余数为 \(0\) 表示是 \(K\) 的倍数,最短路径长度就是最小数位和。
- \(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\) 是好整数。
换一个角度看,从最高位到最低位读,数位是非递减的:
所以“好整数”就是:从最高位到最低位,数位单调不降的数。
例如:
- \(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\)。
因此可以用 BFS 搜索状态:
其中:
- \(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=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\),要求:
这样保证高位到低位非递减。
转移:
新状态为:
如果该状态未访问,则入队,并记录前驱:
代码对应:
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\) 压入栈,然后跳到前驱状态:
直到前驱为 \((-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。
八、总结
本题的关键是:
- 好整数等价于从高位到低位非递减。
- 在模 \(N\) 的余数上做 BFS,状态为 \((余数, 最后一位)\)。
- 最高位从 \(1\sim9\) 开始,后续每位 \(j\ge\) 上一位。
- BFS 第一次遇到余数 \(0\) 的状态就是最小答案。
- 用前驱数组还原整个数字,避免大整数溢出。
核心转移公式:
目标状态:\(m=0\)
[luoguP9751](CSP-J 2023)旅游巴士
P9751 [CSP-J 2023] 旅游巴士 详解
一、题意理解
小 Z 从景区入口(1 号点)走到出口(n 号点)。
约束条件:
- 每条道路单向通行,通过恰好耗时 1 单位时间。
- 每条道路有开放时间 \(a_i\),只有时刻 \(\ge a_i\) 才能通过。
- 到达入口的时刻必须是 \(k\) 的非负整数倍(他乘坐入口巴士到达)。
- 到达出口的时刻也必须是 \(k\) 的非负整数倍(他乘坐出口巴士离开)。
- 他想一直沿道路移动,不想停留。
问题:求最早离开景区的时刻。若无法完成,输出 -1。
二、关键观察
1. 时间状态只需要记录模 \(k\) 的余数
因为最终要求到达出口时时间是 \(k\) 的倍数,所以时间的关键信息是它模 \(k\) 的余数。
设:
目标是 \(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}\):
第二行表示通过道路耗时 1,到达新节点时刻为 \(t'+1\)。
3. 为什么等待 \(k\) 的整数倍
等待 \(k\) 的整数倍时间,时间模 \(k\) 的余数不变。
这样,从入口出发时余数为 0,每走一条边余数加 1,最终到达出口时若余数回到 0,则时刻是 \(k\) 的倍数。
代码中状态转移为:
三、算法: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)\):
- 当前时刻
nd = d[x][i]。 - 若
nd < a,等到最小的 \(t'\ge a\) 且 \(t'\equiv nd\pmod{k}\)。 - 通过道路,时刻变为
nd+1。 - 到达节点 \(y\),余数变为
(i+1)%k。 - 若新时刻更优,更新并入队。
输出答案
if(d[n][0]>=0x3f3f3f3f) d[n][0]=-1;
cout<<d[n][0];
若 \(d[n][0]\) 仍为无穷大,说明无法到达,输出 -1。
五、总结
本题的核心是:
- 把“时间”按模 \(k\) 的余数分层,状态为 \((节点,时间\bmod k)\)。
- 走一条边时余数加 1。
- 若当前时刻小于道路开放时间,等到最小的 \(t'\ge a\) 且 \(t'\equiv t\pmod{k}\),保持余数不变。
- 用 Dijkstra 求 \(d[n][0]\),即到达出口且时刻为 \(k\) 的倍数的最早时刻。
转移公式:
答案:
若不可达,输出 -1。
CCPChenan2025G Plus Xor
一、题目大意
给定整数 \(a,b,c\),其中 \(b\) 固定不变。你可以对 \(a\) 任意次执行以下两种操作:
- \(a\leftarrow a+b\)
- \(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\) 的这两个余数就可以确定。
于是把状态定义为:
状态数最多为:
在总限制 \(\sum b^2\le 10^6\) 下可以接受。
2. 为什么每个状态只保留最小的 \(a\)
对于同一个状态 \((x,y)\),可能有很多个可达的 \(a\) 值,它们都满足:
设其中两个值为 \(a_1<a_2\)。由于它们同状态,差值:
同时是 \(1024\) 和 \(b\) 的倍数,因此是:
的倍数。
而 \(b<1024\),所以异或 \(b\) 只影响低 \(10\) 位。因为 \(a_1,a_2\) 低 \(10\) 位相同,所以:
同理:
也就是说,从较大的 \(a_2\) 出发,操作后得到的结果,恰好比从较小的 \(a_1\) 出发得到的结果大同样的差值,并且新状态相同。
因此对于每个状态,只需要保留最小可达的 \(a\)。记:
3. 为什么判断 \(d[c\bmod 1024][c\bmod b]\le c\) 即可
如果存在可达的 \(a_0\) 满足:
那么:
同时是 \(1024\) 和 \(b\) 的倍数,所以是:
的倍数。
而重复执行 \(\frac{L}{b}\) 次 \(a\leftarrow a+b\),总增加量恰好为 \(L\),并且 \(a\bmod 1024\) 与 \(a\bmod b\) 都回到原来的值。
因此可以从 \(a_0\) 出发,每次增加 \(b\),经过若干轮后恰好到达 \(c\)。
所以只要:
就说明 \(c\) 可达,输出 YES;否则输出 NO。
三、算法流程
-
读入 \(a,b,c\)。
-
清空当前 \(b\) 对应的状态数组。
-
初始状态:
\[d[a\bmod 1024][a\bmod b]=a \]并把 \((a,a\bmod 1024,a\bmod b)\) 压入小根堆。
-
不断取出当前 \(a\) 最小的状态 \((x,y)\):
- 执行 \(a\leftarrow a+b\),得到新状态和新值;
- 执行 \(a\leftarrow a\oplus b\),得到新状态和新值;
- 若新值更小,则更新并入堆。
-
最后检查:
\[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\) 的余数不变,所以:
新余数:
操作二:\(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}});
}
新值:
新状态:
判断答案
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\)。
核心判断:
成立则输出 YES,否则输出 NO。
abc482F - Mod M Divisor
题目
给定正整数 \(N,M\),对每个 \(k=0,1,\dots,M-1\),求最小的正整数 \(t\),使得 \(N^t\) 存在一个正因子 \(x\) 满足:
如果不存在这样的 \(t\),输出 -1。
数据范围:
- \(1\le N\le 10^{14}\)
- \(1\le M\le 4\times10^5\)
核心观察
1. 分解 \(N\) 的质因数
设:
其中 \(p_i\) 是不同质数,\(\alpha_i\ge1\)。
那么 \(N^t\) 的任意正因子都可以写成:
其中:
也就是说,对于固定的 \(t\),第 \(i\) 个质因子的指数上界是 \(t\alpha_i\)。
2. 问题转化为“最小轮数”
对于一个因子 \(x\),设它使用的指数为 \(e_0,e_1,\dots,e_{s-1}\)。
它需要的最小 \(t\) 满足:
对所有 \(i\) 都成立。因此:
我们的目标是:对每个余数 \(k\),找到一组指数 \(e_i\),使得:
并且让上面的最大值尽可能小。
3. 按“轮”扩展指数
可以这样理解:
- 每一轮中,对于每个质因子 \(p_i\),可以选择给它增加 \(0,1,\dots,\alpha_i\) 个指数。
- 完成 \(t\) 轮后,每个质因子的总指数上界正好是 \(t\alpha_i\)。
- 所以 \(t\) 就是轮数。
于是可以按质因子顺序,逐轮构造因子,并用 0-1 BFS 求最小的轮数。
状态设计
定义:
表示当前构造出的因子模 \(M\) 的余数为 \(v\),已经完成了若干轮,当前处理到第 \(i\) 个质因子时,所需的最小轮数。
这里的 \(i\) 表示:
- \(i=s-1\):当前这一轮已经处理完所有质因子,下一步将开始新的一轮;
- \(0\le i<s-1\):当前这一轮已经处理完第 \(0,1,\dots,i\) 个质因子,下一步处理第 \(i+1\) 个质因子。
初始状态:
因为因子 \(1\) 的余数为 \(1\),且还没有开始任何一轮,轮数为 \(0\)。
转移
情况一:\(i=s-1\)
当前一轮已经结束,需要开始新的一轮,轮数加 \(1\)。
然后处理第 \(0\) 个质因子,即选择给它增加 \(j\in[0,\alpha_0]\) 个指数:
转移:
边权为 \(1\)。
情况二:\(i<s-1\)
当前这一轮还没有结束,不增加轮数。
处理下一个质因子,即第 \(i+1\) 个质因子,选择给它增加 \(j\in[0,\alpha_{i+1}]\) 个指数:
转移:
边权为 \(0\)。
为什么用 0-1 BFS
转移的边权只有两种:
- 开始新的一轮:边权 \(1\);
- 同一轮内继续处理下一个质因子:边权 \(0\)。
因此可以用双端队列实现 0-1 BFS:
- 边权为 \(0\) 时,压入队首;
- 边权为 \(1\) 时,压入队尾。
这样每个状态第一次被取出时,就得到了最小轮数。
答案统计
对每个余数 \(k=0,1,\dots,M-1\):
因为可能在某一轮中间就已经得到目标余数,不需要完成整轮。
特别地:
- 若 \(\text{ans}_k=+\infty\),说明不可达,输出
-1。 - 对于 \(k=1\),因子 \(1\) 总是可达的,且题目要求 \(t\) 为正整数,所以最小 \(t=1\),直接令答案为 \(1\)。
边界情况
\(M=1\)
所有数模 \(1\) 都为 \(0\),因子 \(1\) 就满足条件,最小 \(t=1\)。
输出:
1
\(N=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\) 分解成:
存储在 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;
}
本文来自博客园,作者:Aries2011,转载请注明原文链接:https://www.cnblogs.com/Aries2011/p/23526774

浙公网安备 33010602011771号