同余最短路的转圈法

学习自 Alex_Wei 的博客

同余最短路模板题:[国家集训队] 墨墨的等式

已知长为 \(n\) 的序列 \(a\)。对于不定方程 \(\sum\limits_{i=1}^na_ix_i=b\;(x_i\ge0)\),问有多少 \(b\in[l,r]\) 可以使得方程有解。

\(n\le12\)\(a_i\le5\times10^5\)\(l,r\le10^{12}\)

本文默认取模得到的结果都是自然数

首先把询问拆成区间 \([0,l-1]\)\([0,r]\),只考虑求区间 \([0,l]\) 的合法数。

设集合 \(B=\{x\in\mathbb Z\mid 当\,b=x\,时原方程有解\}\)。显然如果 \(r\in B\) ,那么 \(r+a_i\in B\)

设集合 \(\mathrm Z_p^i=\{x\in \mathbb{Z}\mid x\equiv i\pmod p\}\),即模 \(p\)\(i\) 的剩余类。显然有结论:

\[i\equiv j\!\!\!\pmod p\iff \mathrm Z_p^i=\mathrm Z_p^j \]

\[\bigcup\limits_{i=k}^{k+p-1}\mathrm Z_p^i=\mathbb Z \]

我们随便选一个 \(a_i\),方便起见我们选择 \(a_1\)。设 \(f(i)\) 表示 \(\mathrm Z_{a_1}^i\cap B\) 的最小元素,方便起见令 \(i\in[0,a_1)\)

显然 \(\forall k\ge 0,f(i)+ka_1\in\mathrm Z_{a_1}^i\cap B\)。因此如果我们求出了 \(f(i)\),对于每个 \(i\) 我们都可以算出满足 \(Z_{a_1}^i\cap B\cap[0,l]\) 的大小,即模 \(a_1\)\(i\) 的答案数。因为 \(\bigcup\limits_{i=0}^{a_1-1}\mathrm Z_{a_1}^i=\mathbb Z\),所以这些答案加起来不重不漏。

考虑如何求 \(f_i\),不难发现:

\[f(0)=0 \]

\[f(i)=\min_{j=2}^nf((i-a_j)\bmod a_1)+a_j \]

一种方法是建图,用最短路解决。点数为 \(a_i\),边数为 \(na_i\),时间复杂度是 Dijkstra 的 \(O(na_1\log na_1)\) 或 SPFA 的 \(O(na_1^2)\)

但是还有另一种方式,考虑逐个添加 \(a\),设 \(g(j,i)\) 表示只有前 \(j\) 个数时的 \(f(i)\),现在考虑从 \(j-1\)\(j\),添加一个 \(a_j\) 会发生的转移:

\[g(j-1,i)+ka_j\rightarrow g(j,(i+ka_j)\bmod a_1)\quad(k\ge0) \]

(箭头表示“更新”)

对于每个 \(i\),考虑从 \(0\) 开始枚举 \(k\),不难发现 \(k=\dfrac{a_1}{\gcd(a_1,a_j)}\) 时就可以停下了,因为此时 \(i+ka_j\equiv i\pmod{a_1}\),再往下走只会把之前更新过的再更新一遍,但是由于此时的 \(k\) 比上一次更新时大,所以这些更新不会再起效果。

由于模数相同时,不同的剩余类交集为空,所以考虑对于每个剩余类中的下标分别更新。形式化地说,就是对每个 \(x\in[0,a_j)\),分别更新 \(f_i\),其中 \(i\in\mathrm Z_{a_j}^x\)。从完全背包的角度考虑这个问题,压掉第一维,得到转移 \(f(i)+a_j\rightarrow f((i+a_j)\bmod a_{1})\)。从 \(i\) 开始走 \(k=\dfrac{a_1}{\gcd(a_1,a_j)}\) 步可以更新完 \(i\) 能更新的点,那么从 \(i\) 开始走 \(2k\) 步,也就是“多走一圈”,就相当于从 \(i\) 所在的剩余类中的每个点都走了 \(k\) 步,也就可以更新完这个剩余类中的每个点。

for(int p = i, c = 0; c < 2; c += p == i) {
    gmin(f[(p + a[j]) % a[1]], f[p] + a[j]);
    p = (p + a[j]) % a[1];
}

每次可以更新 \(\dfrac{a_1}{\gcd(a_1,a_j)}\) 个位置,所以只需要枚举前 \(\gcd(a_1,a_j)\) 个位置进行更新即可。

时间复杂度 \(\Theta(na_1)\)。可以先排序让 \(a_1\) 最小,减小常数。

#include<bits/stdc++.h>
#define endl '\n'
#define rep(i, s, e) for(int i = s, i##E = e; i <= i##E; ++i)
#define int long long
#define gmin(x, y) (x = min(x, y))
using namespace std;
constexpr int N = 15, A = 5e5;
int n, a[N], l, r, f[A];
signed main() {
#ifdef ONLINE_JUDGE
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
#endif
    cin >> n >> l >> r;
    rep(i, 1, n) cin >> a[i];
    sort(a + 1, a + n + 1);
    memset(f, 0x3f, sizeof f);
    f[0] = 0;
    rep(j, 2, n) rep(i, 0, __gcd(a[1], a[j]) - 1)
        for(int p = i, c = 0; c < 2; c += p == i) {
            gmin(f[(p + a[j]) % a[1]], f[p] + a[j]);
            p = (p + a[j]) % a[1];
        }
    int ans = 0;
    --l;
    rep(i, 0, a[1] - 1) {
        if(l >= f[i]) ans -= (l - f[i]) / a[1] + 1;
        if(r >= f[i]) ans += (r - f[i]) / a[1] + 1;
    }
    cout << ans << endl;
    return 0;
}
posted @ 2023-07-18 18:49  untitled0  阅读(156)  评论(0)    收藏  举报