【算法笔记】一步一步推出来的同余最短路优化思路(千字长文,超详细)

整理的算法模板合集: ACM模板


同余最短路

同余最短路实际上使用最短路模型来优化DP问题,使用同余模型来优化极大的数据范围。
基本思想:通过同余构造某些状态,状态之间的关系类似于两点之间的带权有向边。


通常是解决给定m个整数,求这m个整数能拼凑出多少的其他整数(这m个整数可以重复取)或给定m个整数,求这m个整数不能拼凑出的最小(最大)的整数。

我们首先考虑一个问题:

给你一个数M,三个数x,y,z,问 1 1 1 ~ M M M间一共多少个数可以由 1 1 1开始,通过 + x , + y , + z , 归 零 ( 变 成 1 ) +x,+y,+z,归零(变成1) +x+y+z1 四种操作得到( M < = 50 M<= 50 M<=50

我们可以爆搜!
数据范围50,爆搜可以解决所有问题!众生平等!

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<stdio.h>
#include<cmath>
#define debug(x) cout << x << "ok" << endl
typedef long long ll;
const int N = 50000007, M = 5000007, INF = 0x3f3f3f3f;
using namespace std;
int n, m;
ll h, ans;
bool vis[N];
int x, y, z;

void dfs(ll now)
{
    if(now + x > h && now + y > h && now + z > h)return ;

    if(now + x <= h && vis[now + x] == 0){
        vis[now + x] = true;
        ans ++ ;
        dfs(now + x);
    }

    if(now + y <= h && vis[now + y] == 0){
        vis[now + y] = true;
        ans ++ ;
        dfs(now + y);
    }

    if(now + z <= h && vis[now + z] == 0){
        vis[now + z] = true;
        ans ++ ;
        dfs(now + z);
    }
}

int main()
{
    //freopen("1.in", "r", stdin);
    //freopen("1.out", "w", stdout);
    scanf("%lld\n%d%d%d", &h, &x, &y, &z);
    ans = 1;
    dfs(1);
    cout << ans << endl;
    return 0;
}

那么如果我们加强一点呢?

给你一个数M,三个数x,y,z,问 1 1 1 ~ M M M间一共多少个数可以由1开始,通过+x,+y,+z三种操作得到( M < = 1000 M<= 1000 M<=1000

我们发现这其实就是一个完全背包问题,因为每个操作可以无限使用,我们可以直接开一个数组f[1000],表示能凑到的方案数,最后直接判断一下所有1~M个完全背包。

如果我们在加强一点点呢?

给你一个数M,三个数x,y,z,问 1 1 1 ~ M M M间一共多少个数可以由1开始,通过+x,+y,+z三种操作得到( M < = 1000000000000000000 M<= 1000000000000000000 M<=1000000000000000000

M的范围达到了 1 0 18 10^{18} 1018,如此庞大的数据范围,我们不可能开的下 1 0 18 10^{18} 1018的数组,也不可能循环 1 0 18 10^{18} 1018次,所以我们必须考虑如何优化。

让我们来看一下由这个模型填充出来的一道同余最短路模板,让我们一步一步推出来同余最短路这一优化思路。

例题1:luogu P3403 跳楼机

在这里插入图片描述
1 ≤ h ≤ 2 63 − 1 , 1 ≤ x , y , z ≤ 1 0 5 。 1≤h≤2 ^{63} −1,1 \le x,y,z \le 10^5 。 1h26311x,y,z105

这道题数据达到了 1 0 18 10^{18} 1018,也就是第三种形态。

我们首先考虑如何统计答案,我们知道一个数 n u m num num 如果是由 y , z y,z y,z 构成的,即 n u m = a ∗ y + b ∗ z num=a∗y+b∗z num=ay+bz ,那么如果加上 x x x ,它能到达的楼层数为 ( h − n u m ) / x + 1 (h-num)/x+1 (hnum)/x+1 其中 + 1 +1 +1 是因为要统计不加 x x x 时的答案;

​ 那么如果我们能找到这些数,再去统计不就好了吗,但是会有重复,而且数字太过庞大,无法完全跑出,那么考虑什么时候回重复计数,即当 n u m 1 = n u m 2 + a x num1=num2+ax num1=num2+ax ,此时 n u m 1 num1 num1 的贡献已经被 n u m 2 num2 num2 统计过,再观察两个数字,发现 n u m 1 % x = n u m 2 % x num1 \% x=num2 \% x num1%x=num2%x,那么我们只要找到余数相同中最小值即可,余数共有 x x x 种,也就是x的剩余系。这样我们就可以把庞大的 1 0 18 10^{18} 1018缩小到 x x x 的范围里( 1 0 5 10^5 105)。

  • 我们使用同余的思想缩小了范围!

我们考虑题目中操作2和操作3(其实哪个都行,留下最小的那个最优) % x \%x %x 能到达的楼层,可以发现最终答案一定落在 x x x 的剩余系内。当我们知道这些合法的剩余系的时候,就可以不断累加x的值直到最大高度来求出可以到达的不同高度。

如何求出只用操作2,3在每个剩余系到达的最小高度?(因为更高的可以由最小高度递推出来,上面说了) 。我们设 f ( k ) f(k) f(k)表示在 x x x的剩余系内到达的楼层,根据DP的思想,如果此时使用操作2,3那么可以得出:

f ( ( k + y ) % x ) = m i n { f ( k ) + y } f((k+y)\%x)=min\{f(k)+y\} f((k+y)%x)=min{f(k)+y}
f ( ( k + z ) % x ) = m i n { f ( k ) + z } f((k+z)\%x)=min\{f(k)+z\} f((k+z)%x)=min{f(k)+z}

物理意义就是 ( k + y ) % x (k+y)\%x (k+y)%x能到达的楼层实际上就是由k层使用操作2往上爬y层到达的,也就是从往上爬y层递推过来的(转移过来的),我们取最小值即可。

那么怎么求出由底层( k = 0 k=0 k=0)到其他剩余系中元素的最小代价呢?我们发现状态转移方程 f ( ( k + y ) % x ) = m i n { f ( k ) + y } f((k+y)\%x)=min\{f(k)+y\} f((k+y)%x)=min{f(k)+y}和最短路的转移方程 d i s t [ y ] = d i s t [ x ] + z dist[y] = dist[x] + z dist[y]=dist[x]+z非常相似,更重要的是,他们求的都是最小值!(参考差分约束,也是一个问题转移方程与最短路相似然后直接转化为最短路解决)

  • 我们利用最短路来优化了思路!

我们在最短路里的转移方程 d i s t [ y ] = d i s t [ x ] + z dist[y] = dist[x] + z dist[y]=dist[x]+z x x x y y y链接一条权值为 z z z的边,然后跑最短路。同样的,我们构建始点为 k k k,终点为 k + y ) % x k+y)\%x k+y)%x且长度为 y y y 的边,建完图以后,我们从 1 1 1 开始跑一遍最短路(因为我们在第一层)。对于操作3同理。(初始化 f [ 1 ] = 1 f[1]=1 f[1]=1,因为1层也算一个答案)

跑完最短路求出 f f f数组,表示的是通过使用操作2,3能够到达的最小楼层。然后使用最开始讲的公式,扫描一下x的剩余系,求一下答案即可。

解释一下为什么要循环x的整个剩余系,全部都连上边呢?因为我们这里实际上只是把整个框架搭好,因为我们不知道实际上会用到剩余系的哪一个点,所以我们把整个体系全部连上边,然后从起点开始跑最短路即可,最后到不了的点就不会用上。

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<bitset>
#include<queue>
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
const int N = 200007, M = 500007, INF = 0x3f3f3f3f;

ll h, x, y, z;
ll f[N], ans;
bool vis[N];
int head[N], ver[M], nex[M], edge[M], tot;

void add(int x, int y, int z)
{
    ver[tot] = y;
    edge[tot] = z;
    nex[tot] = head[x];
    head[x] = tot ++ ;
}

void spfa()
{
    memset(f, 0x3f, sizeof f);
    queue<int>q;
    q.push(1);
    vis[1] = true;
    f[1] = 1;
    while(q.size()){
        int x = q.front();
        q.pop();
        vis[x] = false;
        for(int i = head[x];~i;i = nex[i]){
            int y = ver[i];
            ll z = edge[i];
            if(f[y] > f[x] + z){
                f[y] = f[x] + z;
                if(!vis[y]){
                    vis[y] = true;
                    q.push(y);
                }
            }
        }
    }
}

int main()
{
    memset(head, -1, sizeof head);
    scanf("%lld\n%lld%lld%lld", &h, &x, &y, &z);
    if(x == 1 || y == 1 || z == 1){
        cout << h << endl;
        return 0;
    }
    for(int i = 0; i < x; ++ i){
        add(i, (i + y) % x, y);//能够到达的楼层高度,+y是往上上y层
        add(i, (i + z) % x, z);
    }
    spfa();
    for(int i = 0; i < x; ++ i){
        if(f[i] <= h){
            ans += (h - f[i]) / x + 1;//+1是因为还要算f[i]这个楼层
        }
    }
    printf("%lld\n", ans);
    return 0;
}

例题2:luogu P2371 [国家集训队]墨墨的等式

在这里插入图片描述

这道题跟上一道题基本上一摸一样只不过有一点细微的差别

  • 这里不再是三个操作,而是改成了n种操作,思路是同样的,我们直接
  • 求的是一个区间的“楼层数”。
  • 从0开始,因为等式是从0开始的。
  • 初始化时 d i s t [ 0 ] = 0 dist[0] = 0 dist[0]=0
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<queue>
#define debug(x) cout << x << "ok" << endl
typedef long long ll;
#define file freopen("1.in", "r", stdin);freopen("1.out", "w", stdout);
const int N = 500007, M = 5000007, INF = 0x3f3f3f3f;
using namespace std;

ll n, m, l, r;
ll dist[N];
ll f[N];
ll head[N], ver[M], nex[M], tot;
ll edge[M];
ll a[N];
bool vis[N];
queue<int>q;

void init()
{
    memset(head, -1, sizeof head);
    tot = 0;
}

void add(ll x, ll y, ll z)
{
    ver[tot] = y;
    edge[tot] = z;
    nex[tot] = head[x];
    head[x] = tot ++ ;
}

void spfa(int s)//本题是从0开始
{
    memset(dist, 0x3f, sizeof dist);
    vis[s] = 1;
    dist[s] = 0;//这里存的是大小
    q.push(s);
    while(q.size()){
        int x = q.front();
        q.pop();
        vis[x] = 0;
        for(int i = head[x] ;~i; i = nex[i]){
            int y = ver[i];
            ll z = edge[i];
            if(dist[y] > dist[x] + z){
                dist[y] = dist[x] + z;
                if(!vis[y]){
                    vis[y] = 1;
                    q.push(y);
                }
            }
        }
    }
}

ll query(ll m)
{
    ll res = 0;
    for(int i = 0; i < a[1]; ++ i){
        if(dist[i] <= m){
            res += (m - dist[i]) / a[1] + 1;
        }
    }
    return res;
}

int main()
{
    init();
    scanf("%lld%lld%ld", &n, &l, &r);

    for(int i = 1; i <= n; ++ i){
        scanf("%lld", &a[i]);
        if(a[i] == 0)
            i -- , n -- ;
    }
    sort(a + 1, a + 1 + n);

    for(int i = 0; i < a[1]; ++ i){
        for(int j = 2; j <= n; ++ j){
            add(i, (i + a[j]) % a[1], a[j]);
        }
    }
    spfa(0);

    printf("%lld\n", query(r) - query(l - 1));
    return 0;
}

例题3:luogu P2662 牛场围栏

在这里插入图片描述

https://www.luogu.com.cn/problem/P2662

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 撸撸猫 设计师:设计师小姐姐 返回首页