同余最短路学习笔记

重构于 2023.10.5。

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

概述

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

基本做法

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

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

题意:给定 \(n\) 个整数 \(a_i\) 与范围 \(l,r\),问 \(S\in [l,r]\) 中有多少个满足 \(\sum\limits_{i=1}^n a_ix_i=S\) 有非负整数解。

\(a_1=d\),那么我们在模 \(d\) 意义下考虑问题。设 \(f_i\) 表示所有 \(\bmod\ d=i\) 的数中最小能被构造出的数。
那么加入多少个 \(a_1\)\(f\) 数组没有影响,用 \(a_{2~n}\) 依次更新答案。那么对于 \(a_i\),有:

\[f_{(x+a_i)\bmod d}=\min\{f_{(x+a_i)\bmod d},f_x+a_i\} \]

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

那么统计 \(S\le x\) 中存在正整数解的个数时,对于每个 \(f_i\),它加上若干个 \(d\) 的数都能被表示出来。答案即为 \(\sum (\lfloor\frac{x-f_i}{d}\rfloor +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+a_i)\bmod d}=\min\{f_{(x+a_i)\bmod d},f_x+a_i\} \]

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

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

板子。
\(d_i\) 表示模 \(x\)\(i\) 的能到达的最小楼层。
那么

  • \(i\xrightarrow{y} (i+y)\mod x\)
  • \(i\xrightarrow{z} (i+z)\mod x\)

起点为 \(d_1=1\)

P2662 牛场围栏

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

ARC084B Small Multiple

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

  • \(i\xrightarrow{0} (i\times 10) \bmod k\)
  • \(i\xrightarrow{1} (i+1)\bmod k\)

初始条件为 \(dis_1=1\),答案为 \(dis_0\)

AGC057D Sum Avoidance

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

Part 1.

引理 1. 设 \(|A|\) 表示集合 \(A\) 的元素个数,则 \(|A|=\left\lfloor \frac{S-1}{2}\right\rfloor\)

首先我们证明 \(|A|\) 的上界:若 \(i\in A\),则必有 \(S-i\notin A\)。同理,若 \(S\bmod 2=0\),则有 \(\frac{S}{2}\notin A\)。故我们可以把 \([1,S)\) 分为 \(\left\lfloor \frac{S-1}{2}\right\rfloor\) 对数,其中每对数至多有一个被选择。

\(A=\{\left\lfloor \frac{S-1}{2}\right\rfloor+1,\dots,S-1,S\}\),则 \(A\) 中任意两个元素之和 \(> S\),该构造必然合法。即对于任意的 \(S\),我们都能构造出至少一组令 \(|A|\) 取到上界的解。

也就是说我们最后的答案集合中,对于 \(i\neq \frac{S}{2}\)\(i\in A\)\(S-i\in A\) 必然恰好满足其一。令 \(A\)\(\le \left\lfloor \frac{S-1}{2}\right\rfloor\) 的元素构成集合 \(B\),则我们可以在只知道 \(B\) 的情况下还原出 \(A\)

引理 2. 若 \(a,b\in B,a+b\le \left\lfloor \frac{S-1}{2}\right\rfloor\),则 \(a+b\in B\)

考虑反证,若 \(a+b\notin B\),则 \(S-a-b\in A\)。又因为 \(a,b\in A\)\(A\) 集合能组合出 \(S\),不合法。

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

依然反证,若集合 \(B\) 合法,但集合 \(A\) 不合法,则代表存在一个 \(x> \left\lfloor \frac{S-1}{2}\right\rfloor\),能与 \(B\) 中的若干个元素组合出 \(S\)。那么有 \(S-x\notin B\),且 \(B\) 中元素可以组合出 \(S-x\)。这与引理 2 矛盾。

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

Part 2.

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

考虑第一个被加进 \(B\) 的数,不难发现它是第一个不是 \(S\) 的约数的数。那么设这个数为 \(d\),则有 \(\text{lcm}(1,2,\dots d-1) \mid S\)。计算得到当 \(S\) 取上界 \(10^{18}\) 时,仍有 \(d\le 43\)

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

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

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

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

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

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

\[\text{for}\ i\in [1,d),\ f_{(x+ix)\bmod d}\gets f_x+ ix \]

枚举 \(x\),则 \(f_{S\bmod d}\) 更新后的值随着 \(v\) 的增大单调不降。可以考虑求出 \(v\) 的下界:

\[v_x=\max_{i=1}^{d-1} \left\lfloor\frac{f_{(S-ix)\bmod d}}{i}\right\rfloor +1 \]

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

Part 3.

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

对于给定的 \(x\),则求得集合 \(B\)\(\le x\) 的元素个数为

\[\sum_{i=0}^d \left\lfloor\frac{x-f_i}{d}\right\rfloor+[i\neq 0] \]

那么对于 \(\le \left\lfloor\frac{S-1}{2}\right\rfloor\) 的答案我们可以直接二分出第 \(k\) 小的值,另一半也可以类似地反过来二分。

submission

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值