bzoj5213: [Zjoi2018]迷宫

好题!话说省选的都开始构造了吗

由于有K的倍数的限制所以不妨取模,先建K个点表示0~K-1这些数,第i个点向[i*m,i*m+m]建边。不难发现这是合法的但不一定是最优的

考虑合并等价的点,首先从直观上考虑,当两个点能够转移到的点相同时,它们一定可以合并,但是能够合并的点远远不止这些

考虑一般化,对于两个节点x,y,假设x*m^q=y*m^q (mod K) 那么只要在q步中x和y没有到达0节点的方案,那么x和y就可以合并

具体的,首先0一定不能被删掉,现在考虑q=1时,1~K-1中等价点

把所有数乘m,只有gcd(m,K)的倍数才能表示出来,对于变成同一个数的点就可以去重了(不过并不需要具体实行这一步骤)

然后,对于能够表示出的能够到达0节点的点,无论如何都没有办法合并,把其中还存在的点计入答案(q=1都存在,但是q递增后就变化了)

让q++,把乘完m得出的每个不同的数拿出来继续进行上述操作,每一轮相当于把在第q轮可以到达0节点且在之前的轮中没有被删的节点计入答案,再把恰好在第q轮等价的点去重(感觉用unique表达更贴切)

 

现在我们目标是快速模拟这个过程,令f(l,K),表示现在要解决的数值域为[1,l],模数为K,考虑如何递归求解

若l<=K/d,没有溢出不会相交,直接返回l即可

仅考虑q=1的情况,在后期继续递归的时候再考虑满足条件。对于以前可以到达0的数为[(K-l),K],那么此时已经可以到达0的数为[K-m*(K-l),K],在这个值域的数的个数为m*(K-l)/gcd(m,K),计入答案(这里可能有点玄学,可以先看下面再回来看)

为了保证值域连续,当把数都变成gcd(m,K)的倍数后,令所有数都除以gcd(m,K)变为连续,此时上界为(K-m*(K-l))/gcd(m,K)(注意m*(K-l)是可以到达0的数的个数,是要保留的部分不参与递归了。用总数减去保留的数量剩下的再变成压缩同余系),相应的同余系大小K也应该变成K/gcd(m,K)

最终继续递归求f((K-m*(K-l))/gcd(m,K),K/gcd(m,K)),注意会有l>K的情况,此时会取遍K/gcd(m,K)直接返回即可

有一个疑问是对于值的改变是否m也应该随之改变?

ccosi(远行客)的说法:(感谢大佬的解答)

你是不是觉得应该乘上m/d?
事实上(k-l)这里已经/d了,所以是直接乘m,下一层的k-m(k-l)就是上一层的k/d-m((k-l)/d)

ta的blog

我的理解是对于每个数我们是乘上而不是加上m,那么域的变化是不影响乘法的

#include<cstdio>
#include<iostream>
#include<cstring>
#include<cstdlib>
#include<algorithm>
#include<cmath>
using namespace std;
typedef long long LL;
LL gcd(LL a,LL b){return (a==0)?b:gcd(b%a,a);}
 
LL m;
LL solve(LL l,LL K)
{
    LL d=gcd(m,K);
    if(l<=K/d)return l;
    if(K<=(double)m*(K-l))return K/d;
    else return m/d*(K-l)+solve((K-m*(K-l))/d,K/d);
}
 
int main()
{
    int T;
    scanf("%d",&T);
    while(T--)
    {
        LL K;
        scanf("%lld%lld",&m,&K);
        printf("%lld
",solve(K-1,K)+1);
    }
     
    return 0;
}
原文地址:https://www.cnblogs.com/AKCqhzdy/p/10710941.html