同余最短路学习笔记
重构于 2023.10.5。
破防了,怎么什么都记不住什么都要重学。
概述
同余最短路一般用于解决形如「给定一些整数 ,每个数可以多次使用,问是否能相加得到 」的问题。通常 是一个很大的数,不能直接使用完全背包等方法。
这类问题可以利用同余的性质来压缩状态,以优化复杂度。
基本做法
接下来以一道题目为例,说明同余最短路的具体做法。
题意:给定 个整数 与范围 ,问 中有多少个满足 有非负整数解。
设 ,那么我们在模 意义下考虑问题。设 表示所有 的数中最小能被构造出的数。
那么加入多少个 对 数组没有影响,用 依次更新答案。那么对于 ,有:
发现这个东西是类似最短路的形式,那么从这个角度考虑问题。我们把模 的每个值看成一个点, 看成 。
对于原来的转移,则可以认为是从 向 连了一条长度为 的边。
令 ,则可以从该点开始跑最短路,求出所有的 值。
那么统计 中存在正整数解的个数时,对于每个 ,它加上若干个 的数都能被表示出来。答案即为 。
由于同余最短路特殊的连边方式,spfa 的时间复杂度正确性可以保证。但是假 dij(堆优化 spfa)有概率被卡。
const int N=5e5+5,M=13;
int n,l,r,a[N];
struct edge{int nxt,to,w;} e[N*M];
int head[N],cnt;
il void add(int u,int v,int w) {e[++cnt]={head[u],v,w};head[u]=cnt;}
#define pii pair<int,int>
#define fi first
#define se second
priority_queue<pii,vector<pii>,greater<pii> >q;
int dis[N];
il void dij()
{
memset(dis,0x3f,sizeof(dis));
dis[0]=0; q.push(pii(0,0));
while(!q.empty())
{
int u=q.top().se,f=q.top().fi; q.pop();
if(dis[u]!=f) continue;
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(dis[v]>dis[u]+e[i].w)
{
dis[v]=dis[u]+e[i].w;
q.push(pii(dis[v],v));
}
}
}
}
signed main()
{
n=read(),l=read(),r=read();
for(int i=1;i<=n;i++) a[i]=read();
for(int i=2;i<=n;i++)
{
for(int j=0;j<a[1];j++) add(j,(j+a[i])%a[1],a[i]);
}
dij();
int ans=0;
for(int i=0;i<a[1];i++)
{
int cntr=(dis[i]<=r)?(r-dis[i])/a[1]+1:0;
int cntl=(dis[i]<l)?(l-1-dis[i])/a[1]+1:0;
ans+=cntr-cntl;
}
printf("%lld\n",ans);
return 0;
}
转圈 /zhq
同余最短路还在写最短路?不如转圈!/zhq
我们把思路回退到这个式子:
重新观察它的性质。首先显然,根据算法的正确性,最后求出的答案与加入 的顺序无关。
也就是说最终对于每个 ,一定存在一条按 顺序访问的路径能取到最短路。那么依次使用 更新所有的 就能保证正确性。
另外一个性质是只走相同 边的情况下,一个点不会被经过多次,否则图上有负环;那么我们每次找到最小的起始点,绕着所有的 边转移一圈即可。
然而找最小点是麻烦的,所以不用找,从转一圈改成转两圈就可以覆盖所有情况了。
const int N=5e5+5;
int n,m,a[N];
int f[N],l,r,ans;
signed main()
{
n=read(),l=read(),r=read();
for(int i=1;i<=n;i++) a[i]=read();
memset(f,0x3f,sizeof(f)),f[0]=0;
sort(a+1,a+n+1),m=a[1];
for(int i=2;i<=n;i++)
{
for(int j=0,gd=__gcd(a[i],m);j<gd;j++)
{
for(int t=j,c=0;c<2;c+=(t==j))
{
int p=(t+a[i])%m;
f[p]=min(f[p],f[t]+a[i]),t=p;
}
}
}
int ans=0;
for(int i=0;i<a[1];i++)
{
int cntr=(f[i]<=r)?(r-f[i])/a[1]+1:0;
int cntl=(f[i]<l)?(l-1-f[i])/a[1]+1:0;
ans+=cntr-cntl;
}
printf("%lld\n",ans);
return 0;
}
例题
P3403 跳楼机
板子。
设 表示模 为 的能到达的最小楼层。
那么
起点为 。
P2662 牛场围栏
对于至多砍掉 的限制,把每个长度对应的木板都单独拆出来。
同第一题。统计答案时,对于 ,则 的数中最大表示不出来的是 。
枚举 ,对 取最大值即为答案。
ARC084B Small Multiple
任何一个整数都可以由 通过 , 交替操作若干次得到。
观察到,第一种操作不改变数位和,第二种操作使数位和加 。
那么可以连边:
初始条件为 ,答案为 。
AGC057D Sum Avoidance
一年前看了一天没看懂的题,现在终于知道题解在说啥了。
Part 1.
引理 1. 设 表示集合 的元素个数,则 。
首先我们证明 的上界:若 ,则必有 。同理,若 ,则有 。故我们可以把 分为 对数,其中每对数至多有一个被选择。
令 ,则 中任意两个元素之和 ,该构造必然合法。即对于任意的 ,我们都能构造出至少一组令 取到上界的解。
也就是说我们最后的答案集合中,对于 , 和 必然恰好满足其一。令 中 的元素构成集合 ,则我们可以在只知道 的情况下还原出 。
引理 2. 若 ,则 。
考虑反证,若 ,则 。又因为 , 集合能组合出 ,不合法。
引理 3. 若集合 中的元素不能相加得到 ,则它对应的集合 也合法。
依然反证,若集合 合法,但集合 不合法,则代表存在一个 ,能与 中的若干个元素组合出 。那么有 ,且 中元素可以组合出 。这与引理 2 矛盾。
由于 的元素个数对 不会产生影响,至此我们考虑最小化 的字典序即可。
Part 2.
最小化 的字典序,有显然正确的贪心:从小到大枚举每个数,如果加了不会造成不合法,就把它加进 。我们要做的事是快速维护这个过程。
考虑第一个被加进 的数,不难发现它是第一个不是 的约数的数。那么设这个数为 ,则有 。计算得到当 取上界 时,仍有 。
那么我们可以把贪心过程中加入 中的数 分为两种情况:
- 这个数已经可以被 中原有的数表示,根据引理 2 必须加入;
- 这个数不能被表示,且加入后也不能与其它数表示出 ,也应该贪心地加入。
从模 的剩余系角度考虑,若 以第二种方式加入,则它与所有已经在 中的数模 不同余。所以至多有 个数以第二种方式被加入。
考虑维护一个形如同余最短路的东西,设 表示当前最小能被 集合表示的 的数。那么只需求出最后的 数组即可还原出 集合,而数组大小只有 ,复杂度可以接受。
第一种情况对 数组不会产生任何影响,我们只需考虑第二种情况的贡献。
设下一个以第二种方式加入的数为 ,并令 ,那么首先 应当满足 。加入后 合法的充要条件是用 更新数组后仍满足 。用 更新的过程有如下式子:
枚举 ,则 更新后的值随着 的增大单调不降。可以考虑求出 的下界:
那么下一个要添加的 就是 ,添加以后对 数组进行更新即可,重复该过程直到找不到一个合法的 。
Part 3.
根据最终的 数组还原答案。
对于给定的 ,则求得集合 中 的元素个数为
那么对于 的答案我们可以直接二分出第 小的值,另一半也可以类似地反过来二分。
本文来自博客园,作者:樱雪喵,转载请注明原文链接:https://www.cnblogs.com/ying-xue/p/16976289.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 张高兴的大模型开发实战:(一)使用 Selenium 进行网页爬虫
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构