【算法讲19:同余最短路】跳楼机 | 墨墨的等式 | Lazy Running

补充

  • 同余最短路 是一个集 数学同余表达图论最短路 结合的算法。
    不是很难,需要尽早掌握。

例题一

  • 【链接】
    跳楼机 | 洛谷 P3403
  • 【题意】
    你一开始在第一层。一共有 h h h 层。
    你每一次操作,可以向上 x x x 层,或向上 y y y 层,或向上 z z z 层,或回到第一层。
    问你一共有多少层楼是你可以到达的
  • 【范围】
    1 ≤ h ≤ 2 63 − 1 1\le h\le 2^{63}-1 1h2631
    1 ≤ x , y , z ≤ 1 0 5 1\le x,y,z\le 10^5 1x,y,z105

思路一:动态规划

  • d p [ x ] dp[x] dp[x] ,其中 d p [ x ] = 1 dp[x]=1 dp[x]=1 表示能到达这一层楼,否则表示不能到达这一层楼。
    我们得到简单的状态转移方程:
    d p [ t ] = d p [ t − x ]   ∣   d p [ t − y ]   ∣   d p [ t − z ] dp[t]=dp[t-x]\ |\ dp[t-y] \ |\ dp[t-z] dp[t]=dp[tx]  dp[ty]  dp[tz]
    简单的初始化: d p [ 1 ] = 1 , d p [ t < 1 ] = 0 dp[1]=1,dp[t<1]=0 dp[1]=1,dp[t<1]=0,这里下标记作可以为负
  • 但是由于我们的 h h h 达到了很大的数量级,故这种做法时间复杂度为 O ( h ) O(h) O(h),只能得到 T L E \color{red}TLE TLE

⌈ \lceil 同余最短路 ⌋ \rfloor

  • 考虑到,如果我们通过某些操作到达了第 i i i 层, 那么我们可以到达 i + a x i+ax i+ax 层,其中 a ∈ N a\in \mathbb{N} aN
    那么,如果我们能到达 i i i 层,这一层及上面还可以到达的层数 ⌊ h − i x ⌋ + 1 \lfloor\dfrac{h-i}{x}\rfloor+1 xhi+1 层。
    容易得到,如果能到达 i + a x i+ax i+ax 层,其中 a ∈ N a\in \mathbb{N} aN,那么必然能到达 i i i 层。
    不妨,我们求出最小的一些 i i i使得 i i i 层为不用操作 x x x 就能到达的最小楼层。
    从同余的角度分析,即求出每一个 i ∈ [ 0 , a ) i\in[0,a) i[0,a),算出到达 t % a = i t\%a=i t%a=i 层的最小的 t t t
  • 我们设 d p [ i ] dp[i] dp[i] 表示 min ⁡ t { t % a = i 满 足 t = b y + c z , 其 中 b , c ∈ N } \min_t\{t\%a=i\quad 满足\quad t=by+cz,其中 b,c\in \mathbb{N}\} mint{t%a=it=by+czb,cN}
    式子按照数学的角度设出来比较合理,但是做起来比较复杂。此时我们要按照图论的角度去分析了。
    如果我们能到达 i i i 层,那么我们还能到达 i + y i+y i+y 层和 i + z i+z i+z 层,当然前提是层数 ≤ h \le h h
    按照 d p dp dp 式子建图,即 i → 边 权 为 y ( i + y ) % a i\overset{边权为y}{\rightarrow}(i+y)\%a iy(i+y)%a i → 边 权 为 z ( i + z ) % a i\overset{边权为z}{\rightarrow}(i+z)\%a iz(i+z)%a
    容易得到这就是跑一个最短路。点数和边数都是 1 e 5 1e5 1e5 级别的,用 d i j k s t r a dijkstra dijkstra 即可。

核心代码 A c c e p t e d \color{green}Accepted Accepted

  • 时间复杂度: O ( a log ⁡ a ) O(a\log a) O(aloga)
/*
 _            __   __          _          _
| |           \ \ / /         | |        (_)
| |__  _   _   \ V /__ _ _ __ | |     ___ _
| '_ \| | | |   \ // _` | '_ \| |    / _ \ |
| |_) | |_| |   | | (_| | | | | |___|  __/ |
|_.__/ \__, |   \_/\__,_|_| |_\_____/\___|_|
        __/ |
       |___/
*/
ll h;int a,b,c;
ll dp[MAX];
vector<int>V[MAX],G[MAX];
priority_queue<pair<ll,ll> >Q;

void dijkstra(int s){
    Q.push(make_pair(-1,s));
    while(!Q.empty()){
        int x = Q.top().second;
        ll t = -Q.top().first;
        Q.pop();
        if(t > dp[x])continue;
        for(auto it : V[x]){
            if(dp[it] > dp[x] + a && dp[x] + a <= h){
                dp[it] = dp[x] + a;
                Q.push(make_pair(-dp[it],it));
            }
        }
        for(auto it : G[x]){
            if(dp[it] > dp[x] + b && dp[x] + b <= h){
                dp[it] = dp[x] + b;
                Q.push(make_pair(-dp[it],it));
            }
        }
    }
}

int main()
{
    cin >> h >> a >> b >> c;

    for(int i = 0;i < c;++i)dp[i] = LINF;
    dp[1] = 1;
    for(int i = 0;i <= c;++i){
        V[i].push_back((i+a)%c);
        G[i].push_back((i+b)%c);
    }
    dijkstra(1);
    ll res = 0;
    for(int i = 0;i < c;++i){
        if(dp[i] == LINF)continue;
        res = res + ((h - dp[i]) / c) + 1;
    }
    cout << res;
    return 0;
}

例题二

  • 【链接】墨墨的等式 | 洛谷 P2371
  • 【题意】
    给定 n , a 1 , ⋯   , a n , l , r n,a_1,\cdots,a_n,l,r n,a1,,an,l,r
    问你有多少组非负解向量 x 1 , ⋯   , x n x_1,\cdots,x_n x1,,xn,满足:
    ∑ i = 1 n a i x i ∈ [ l , r ] \sum_{i=1}^na_ix_i\in[l,r] i=1naixi[l,r]
  • 【范围】
    n ≤ 12 n\le 12 n12
    0 ≤ a i ≤ 5 × 1 0 5 0\le a_i\le 5\times 10^5 0ai5×105
    1 ≤ l ≤ r ≤ 1 0 12 1\le l\le r\le 10^{12} 1lr1012
  • 【思路】
    首先和第一题的跳楼机非常非常像。跳楼机的每种操作次数相当于这里的解向量的元素 x i x_i xi的值。
    为了方便,设 d p [ i ] dp[i] dp[i] 表示 min ⁡ h { h % a 1 = i 并 且 h 可 以 到 达 } \min_h\{h\%a_1=i\quad并且\quad h可以到达\} minh{h%a1=ih}
    然后正常地去建图貌似就没区别了。
    不过它让你求 [ l , r ] [l,r] [l,r] 范围内解的数量,我们只要求出 [ 1 , r ] [1,r] [1,r] 数量减去 [ 1 , l − 1 ] [1,l-1] [1,l1] 数量即可。
  • 【代码】 A c c e p t e d \color{green}Accepted Accepted
    时间复杂度: O ( n × a 1 log ⁡ a 1 ) O(n\times a_1\log a_1) O(n×a1loga1)
/*
 _            __   __          _          _
| |           \ \ / /         | |        (_)
| |__  _   _   \ V /__ _ _ __ | |     ___ _
| '_ \| | | |   \ // _` | '_ \| |    / _ \ |
| |_) | |_| |   | | (_| | | | | |___|  __/ |
|_.__/ \__, |   \_/\__,_|_| |_\_____/\___|_|
        __/ |
       |___/
*/
int n;
int aa[20];
ll dp[MAX];
ll V[MAX][17];
priority_queue<pair<ll,ll> >Q;

void dijkstra(int s,ll h){
    Q.push(make_pair(0,s));
    while(!Q.empty()){
        int x = Q.top().second;
        ll t = -Q.top().first;
        Q.pop();
        if(t > dp[x])continue;
        for(int i = 2;i <= n;++i){
            int it = V[x][i];
            if(dp[it] > dp[x] + aa[i] && dp[x] + aa[i] <= h){
                dp[it] = dp[x] + aa[i];
                Q.push(make_pair(-dp[it],it));
            }
        }

    }
}
ll solve(ll h){
    for(int i = 0;i < aa[1];++i)dp[i] = LINF;
    dp[0] = 0;
    for(int i = 0;i < aa[1];++i){
        for(int j = 2;j <= n;++j){
            V[i][j] = (i+aa[j]) % aa[1];
        }
    }
    dijkstra(0,h);
    ll res = 0;
    for(int i = 0;i < aa[1];++i){
        if(dp[i] == LINF)continue;
        res = res + ((h - dp[i]) / aa[1]) + 1;
    }
    return res;
}
int main()
{
    ll l,r;
    cin >> n >> l >> r;
    for(int i = 1;i <= n;++i)cin >> aa[i];
    ll res = solve(r) - solve(l-1);
    cout << res;
    return 0;
}

例题三

  • 【链接】
    Lazy Running | HDU 6071
  • 【题意】
    有四个点。给出 d 1 , 2 d_{1,2} d1,2 d 2 , 3 d_{2,3} d2,3 d 3 , 4 d_{3,4} d3,4 d 4 , 1 d_{4,1} d4,1,表示两个点之间的无向边的长度。
    问你,从点二出发到点二结束,路径距离和 ≥ K \ge K K 的最小值为多少。
  • 【范围】
    1 ≤ d ≤ 3 × 1 0 4 1\le d\le 3\times 10^4 1d3×104
    1 ≤ K ≤ 1 0 18 1\le K\le 10^{18} 1K1018
  • 【思路】
    w = min ⁡ ( d 1 , 2 , d 2 , 3 ) w=\min(d_{1,2},d_{2,3}) w=min(d1,2,d2,3)
    如果你跑到 2 2 2 号点已经走了 x x x 距离的话,你可以通过来回跳跃,每次增加 2 w 2w 2w 的方式到达 2 2 2 号点。
    于是,此时最短路长度为: x + 2 w t ≥ K x+2wt\ge K x+2wtK,满足 t ≥ 0 t\ge0 t0
    就跟跳楼机类似。
    我们只需要求出 i ∈ [ 0 , 2 w ) i\in[0,2w) i[0,2w),到每个节点 v v v 的路径长模 i i i 的最小值 d p [ x ] [ i ] dp[x][i] dp[x][i] 即可。
    然后我们需要求得所有的 d p [ 2 ] [ i ] dp[2][i] dp[2][i],然后求出最小的 t ≥ 0 t\ge0 t0 满足 x + 2 w t ≥ K x+2wt\ge K x+2wtK
    那么答案即为 min ⁡ { x + 2 w t } \min\{x+2wt\} min{x+2wt}
  • 一些小细节:
    虽然我们对于每一个点都有 2 w 2w 2w 个状态,那么我们优先队列是不是需要存 { x , i , d p [ x ] [ i ] } \{x,i,dp[x][i]\} {x,i,dp[x][i]} 呢?这里不需要,因为根据定义得到 d p [ x ] [ i ] % 2 w = i dp[x][i]\%2w=i dp[x][i]%2w=i,我们少存一维状态,可以节省许多时间和内存,防止代码超时。
  • 【代码】 A c c e p t e d \color{green}Accepted Accepted
    时间复杂度: O ( d log ⁡ d ) O(d\log d) O(dlogd)
/*
 _            __   __          _          _
| |           \ \ / /         | |        (_)
| |__  _   _   \ V /__ _ _ __ | |     ___ _
| '_ \| | | |   \ // _` | '_ \| |    / _ \ |
| |_) | |_| |   | | (_| | | | | |___|  __/ |
|_.__/ \__, |   \_/\__,_|_| |_\_____/\___|_|
        __/ |
       |___/
*/
ll K;
int aa[5][5];
ll dp[10][MAX];
struct node{
    int x;
    ll shu;
    bool operator < (const node &ND)const{
        return shu > ND.shu;
    }
};
priority_queue<node>Q;

vector<pair<int,int> >V[5];

void dijkstra(int s,ll w){
    Q.push({s,0});
    while(!Q.empty()){
        int x = Q.top().x;
        int y = Q.top().shu % w;
        ll t = Q.top().shu;
        Q.pop();
        if(t > dp[x][y])continue;
        for(auto it : V[x]){
            int to = it.first;
            int  d = it.second;
            if(dp[to][(t + d) % w] > t + d){
                dp[to][(t + d) % w] = t + d;
                Q.push({to,dp[to][(t + d) % w]});
            }
        }
    }
}
ll solve(){
    ll w = min(aa[1][2],aa[2][3]);
    w = w * 2;
    if(w >= K)return w;
    for(int i = 1;i <= 4;++i)
        for(int j = 0;j < w;++j)
            dp[i][j] = LINF;
    dp[2][0] = 0;
    dijkstra(2,w);
    ll res = LINF;
    for(int i = 0;i < w;++i){
        if(dp[2][i] == LINF)continue;
        if(dp[2][i] >= K){				/// 我们不能允许 t 为负数
            res = min(res,dp[2][i]);
            continue;
        }								/// 简单使用 floor 函数会导致精度出现错误
        res = min(res,dp[2][i] + w * (((K - dp[2][i]) / w) + ((K - dp[2][i]) % w != 0 ? 1LL : 0LL)));
    }
    return res;
}
int main()
{

    int T;cin >> T;
    while(T--){
        cin >> K >> aa[1][2] >> aa[2][3] >> aa[3][4] >> aa[4][1];
        aa[2][1] = aa[1][2];
        aa[3][2] = aa[2][3];
        aa[4][3] = aa[3][4];
        aa[1][4] = aa[4][1];
        for(int i = 1;i <= 4;++i)V[i].clear();

        for(int i = 1;i <= 4;++i){
            int to = i % 4 + 1;
            V[i].push_back(make_pair(to,aa[i][to]));
            V[to].push_back(make_pair(i,aa[i][to]));
        }
        cout << solve() << endl;
    }
    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值