同余最短路学习笔记

重构于 2023.10.5。

破防了,怎么什么都记不住什么都要重学。

概述

同余最短路一般用于解决形如「给定一些整数 ai,每个数可以多次使用,问是否能相加得到 n」的问题。通常 n 是一个很大的数,不能直接使用完全背包等方法。
这类问题可以利用同余的性质来压缩状态,以优化复杂度。

基本做法

接下来以一道题目为例,说明同余最短路的具体做法。

P2371 [国家集训队]墨墨的等式

题意:给定 n 个整数 ai 与范围 l,r,问 S[l,r] 中有多少个满足 i=1naixi=S 有非负整数解。

a1=d,那么我们在模 d 意义下考虑问题。设 fi 表示所有 mod d=i 的数中最小能被构造出的数。
那么加入多少个 a1f 数组没有影响,用 a2 n 依次更新答案。那么对于 ai,有:

f(x+ai)modd=min{f(x+ai)modd,fx+ai}

发现这个东西是类似最短路的形式,那么从这个角度考虑问题。我们把模 d 的每个值看成一个点,fi 看成 disi
对于原来的转移,则可以认为是从 x(x+ai)modd 连了一条长度为 ai 的边。
f0=0,则可以从该点开始跑最短路,求出所有的 f 值。

那么统计 Sx 中存在正整数解的个数时,对于每个 fi,它加上若干个 d 的数都能被表示出来。答案即为 (xfid+1)
由于同余最短路特殊的连边方式,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
我们把思路回退到这个式子:

f(x+ai)modd=min{f(x+ai)modd,fx+ai}

重新观察它的性质。首先显然,根据算法的正确性,最后求出的答案与加入 ai 的顺序无关。
也就是说最终对于每个 fi,一定存在一条按 a2,,an 顺序访问的路径能取到最短路。那么依次使用 ai 更新所有的 fi 就能保证正确性。
另外一个性质是只走相同 ai 边的情况下,一个点不会被经过多次,否则图上有负环;那么我们每次找到最小的起始点,绕着所有的 ai 边转移一圈即可。
然而找最小点是麻烦的,所以不用找,从转一圈改成转两圈就可以覆盖所有情况了。

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 跳楼机

板子。
di 表示模 xi 的能到达的最小楼层。
那么

  • iy(i+y)modx
  • iz(i+z)modx

起点为 d1=1

P2662 牛场围栏

对于至多砍掉 m 的限制,把每个长度对应的木板都单独拆出来。
同第一题。统计答案时,对于 di,则 modx=i 的数中最大表示不出来的是 dix
枚举 di,对 dix 取最大值即为答案。

ARC084B Small Multiple

任何一个整数都可以由 1 通过 ×10+1 交替操作若干次得到。
观察到,第一种操作不改变数位和,第二种操作使数位和加 1
那么可以连边:

  • i0(i×10)modk
  • i1(i+1)modk

初始条件为 dis1=1,答案为 dis0

AGC057D Sum Avoidance

一年前看了一天没看懂的题,现在终于知道题解在说啥了。

Part 1.

引理 1. 设 |A| 表示集合 A 的元素个数,则 |A|=S12

首先我们证明 |A| 的上界:若 iA,则必有 SiA。同理,若 Smod2=0,则有 S2A。故我们可以把 [1,S) 分为 S12 对数,其中每对数至多有一个被选择。

A={S12+1,,S1,S},则 A 中任意两个元素之和 >S,该构造必然合法。即对于任意的 S,我们都能构造出至少一组令 |A| 取到上界的解。

也就是说我们最后的答案集合中,对于 iS2iASiA 必然恰好满足其一。令 AS12 的元素构成集合 B,则我们可以在只知道 B 的情况下还原出 A

引理 2. 若 a,bB,a+bS12,则 a+bB

考虑反证,若 a+bB,则 SabA。又因为 a,bAA 集合能组合出 S,不合法。

引理 3. 若集合 B 中的元素不能相加得到 S,则它对应的集合 A 也合法。

依然反证,若集合 B 合法,但集合 A 不合法,则代表存在一个 x>S12,能与 B 中的若干个元素组合出 S。那么有 SxB,且 B 中元素可以组合出 Sx。这与引理 2 矛盾。

由于 B 的元素个数对 |A| 不会产生影响,至此我们考虑最小化 B 的字典序即可。

Part 2.

最小化 B 的字典序,有显然正确的贪心:从小到大枚举每个数,如果加了不会造成不合法,就把它加进 B。我们要做的事是快速维护这个过程。

考虑第一个被加进 B 的数,不难发现它是第一个不是 S 的约数的数。那么设这个数为 d,则有 lcm(1,2,d1)S。计算得到当 S 取上界 1018 时,仍有 d43

那么我们可以把贪心过程中加入 B 中的数 x 分为两种情况:

  • 这个数已经可以被 B 中原有的数表示,根据引理 2 必须加入;
  • 这个数不能被表示,且加入后也不能与其它数表示出 S,也应该贪心地加入。

从模 d 的剩余系角度考虑,若 x 以第二种方式加入,则它与所有已经在 B 中的数模 d 不同余。所以至多有 d 个数以第二种方式被加入。

考虑维护一个形如同余最短路的东西,设 fi 表示当前最小能被 B 集合表示的 mod d=i 的数。那么只需求出最后的 f 数组即可还原出 B 集合,而数组大小只有 d,复杂度可以接受。

第一种情况对 f 数组不会产生任何影响,我们只需考虑第二种情况的贡献。

设下一个以第二种方式加入的数为 v,并令 x=vmodd,那么首先 v 应当满足 fx>v。加入后 B 合法的充要条件是用 v 更新数组后仍满足 fSmodd>S。用 v 更新的过程有如下式子:

for i[1,d), f(x+ix)moddfx+ix

枚举 x,则 fSmodd 更新后的值随着 v 的增大单调不降。可以考虑求出 v 的下界:

vx=maxi=1d1f(Six)moddi+1

那么下一个要添加的 v 就是 minvx,添加以后对 f 数组进行更新即可,重复该过程直到找不到一个合法的 v

Part 3.

根据最终的 f 数组还原答案。

对于给定的 x,则求得集合 Bx 的元素个数为

i=0dxfid+[i0]

那么对于 S12 的答案我们可以直接二分出第 k 小的值,另一半也可以类似地反过来二分。

submission

posted @   樱雪喵  阅读(266)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 张高兴的大模型开发实战:(一)使用 Selenium 进行网页爬虫
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示