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)
我的理解是对于每个数我们是乘上而不是加上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\n",solve(K-1,K)+1); } return 0; }