使用社交账号登录
前言:刷题时意外碰到这题,初见时以为是简单的数论题,求出通项公式再分析即可,但实际又由于出现根号问题无法直接分析。求助好友后才发现是图论题,觉得这种建图方式很有趣,故记录下来,并延申。
题意就是对于一个序列有:
求对于集合 ,x和y不同取值下,无限长的序列中不存在 (即 不被 M 整除)的(x,y)取值个数。
首先这个通项公式很好写出,就是:
其中 是方程 的根, 是由 和 决定的常数。
但是很显然是这个底数 不一定是整数,比如著名的斐波那契数列,其底数就是带有根号的黄金分割数,直接分析还需要考虑用二项定理展开,然后根据具体的 和 的值,消去根号才能分析,实属复杂。
注意到这里的模数M只有1000,尝试暴力会发现,我们需要分别枚举 和 ,然后对于每个独立的(x,y),计算在前M*M项数中是否存在%M==0
但是这样的复杂度是 ,显然TLE。
这里考虑所有的x和y的取值后,序列 的值必然在模数 内
我们作状态 ,那么有转移 ,也就是每个节点有且仅有一条出边(不一定只有一条入边),即外向基环树
那么不同对的(x,y)又应该如何考虑呢?
我们注意到对于 A=1, B=2; 我们取 有
注意这个节点 ,如果我们取 在数值上也可以得到这个结果
也就是说,对于该空间 内,节点的数值既可以表示 对,同时也是
对于该空间 具体的节点是:
当然其具体的连接关系为:
题目求有多少个(x,y)产生的序列不含M的倍数,在这个状态空间中也就是确定一个起点(x,y)沿着其出边一直走, 并使其不能碰到x=0或者y=0,统计这样的路径。但是这样直接遍历每个节点(上面的方法),复杂度还是 ,显然会超。
这里说明下一些应该知道的关系
那么必然地,
我们只要 地遍历节点构造反图,然后从含0节点bfs搜索计数标记节点,然后 即可
本题很巧妙地使用了状态空间将序列的集合和(x,y)初始选点的集合合并起来,使得在搜索图(序列结果)中恰好也统计了选点的个数,很妙!
而且注意到每个状态到下一个状态是唯一的,即每个结点只有一条出边,整个图由若干个 基环树 Functional Components 组成,这意味着从任何点出发,最终都会进入一个环。
下面引出一些数论问题和图论问题的重合部分:
给定 ,求序列从何时开始进入循环,循环长度是多少?
该问题如果使用数论直接解的话十分复杂,这里只给出大概解法:
1) 时
如果 即讨论 的周期和进入周期情况。
我们先把 消掉,考虑同余式 ,那么同时除以其最小公倍数 得到 :
注意到 ,所以 在模 下有逆元,上式同时乘以 的逆元,得到:
然后考虑模m意义下对A幂的影响,提取出关键因子,考虑 的质因数分解,设 ,并定:
也就是将 分解为含整除 的质因数和不含整除 的质因数,那么 式成立当且仅当(CRT):
对于模 的项, ,由欧拉定理有:
即在对于这个模数来说,序列 是循环的,且最小正周期 为 的阶,记作:
这里阶满足 即阶为 的欧拉函数的约数,这样可以用程序快速求出。
例如先初始化阶等于其欧拉函数,尝试是否满足 ,然后不断除以 的每个质因子,直到不能满足余数为1,此时上个阶即为所求。
对于模 的项,由于其所有素因子均能整除 ,设存在足够大的 使得 包含 所需的所有素因子幂次。
即此时 满足:
那么对于之后的 , 仍然满足上述条件。
我们找到其预周期也就是找到最小的 ,使得对于所有 ,都有:
我们记 为 的 进赋值(即x中质因子p的最高幂次),则对于 满足:
因此,进入周期 的最小预周期 为:
若 (即 ),则 ,这也印证了当A和m互质时,直接进入大循环。
2) 时
{% note primary no-icon %}
引理:线性同余方程的齐次化 Homogenization Lemma
给定线性同余递归序列 ,若满足 ,则该序列可以通过坐标平移 完美等价于齐次序列(即 的形式):
其中 为递归映射的不动点。
证明:
我们需要找到一个常数 ,使得当 时, 也等于 。根据递推式:
整理得:
又 ,那么 在模M下有逆元,即:
那么将坐标平移 ,并带入原式,得:
也就是只要找到 ,就可以将原递推式等价于齐次递推式,从而利用上述方法求解。
{% endnote %}
由引理,可得如果 那么我们直接将递推式齐次化(也就是使得C=0),然后化归为前面的问题,求解即可。
但是如果 ,即 没有逆元,我们可以尝试扩大模数,令 ,那么 可以表示为:
神奇地,$z_{n}$ 的递推关系竟然是齐次的,由于z相对于x被扩大了(A-1),那么其模数也应该扩大到 :
这与前面齐次的解法一样。
这个问题就很显然了,我们直接作状态空间 由于是模M的,所以 ,其连接关系就是 。
我们要求的是进入周期的步长 ,即从 出发,第一次到达环的步长。然后环的长度就是周期 。
当然还有直接使用dsu的解法:
这里解释下这个简洁的dsu为什么可以完成:
我们假设有这样一个序列,
这里在x[t-1]进入环,那么dsu从x[0]开始计数,数到x[t+T-1]时计数步数为s,由x=f(x)进入x[t-1]这个已经被dsu标记过,那么此时dsu[t-1]的计数步数就是预周期t,s-t就是周期。
对于质因数分解,我们最直接就是使用试除法,枚举不大于 的质因子,然后不断除,直到不能整除为止,但是时间复杂度是 ,对于大数来说显然是不够的。
我们考虑一个奇合数 其能被分解成 ,那么其必然是两个平方数的差:
这个方法我们可以先让 ,不断向上枚举x,直到 是个完全平方数为止,那么 ,然后 , ,就可以得到 的一个因数分解。
然后继续按照上面的逻辑分解a和b,直到分解到质数为止。
这种方法对于n的两个因子十分接近时,效率会很高。
但是反之,如果n的两个因子差距很大,我们需要枚举到很大的x才能找到这个完全平方数,我们可以考虑同余式:
那么我们只要找到 即可,或者构造 的解,比如随机或者构造平方。
我们考虑利用最大公约数提取因子,如果n整除 (x-y)(x+y) ,这并不代表n一定整除这其中某一个因子。而我们为了质因数分解,肯定不希望(x-y)或者(x+y)不是n的平凡因子(即因子不是1或者n本身),我们需要保证n的因子都分步在这两项中。
我们假设 ($p, q$ 为互异的质数),那么由CRT有:
因为 是质数,那么 是个域,二次方程 只有两个解(域上的多项式性质):
同理,对于模 也有:
也就是在模n的意义下这四个解:
因此我们对于 再加以非平凡解限定的条件 ,此时n的一部分因子必然在左右两项都有分布。
具体的分解过程如下:
即产生了一颗递归树——因子分解树。
for(int i=0;i<M;i++)for(int j=0;j<M;j++)
{
bool ok=true;
vector<int> s(M*M+1);
s[1]=i, s[2]=j;
for(int k=3; k<=M*M; k++)
{
s[k]=((ll)A*s[k-1]+(ll)B*s[k-2])%M;
if(s[k]==0)
{
ok=false;
break;
}
}
if(ok) ans++;
}
inline int encode(int x, int y, int m)
{
return x*m+y;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int m,a,b; cin >> m >> a >> b;
// 这里使用编码存(x,y) x*m+y
vector<vector<int> > rg(m*m);
for(int i=0; i<m; i++)
{
for(int j=0; j<m; j++)
{
int x1=i, y1=j;
int x2=j, y2=((ll)a*j+(ll)b*i)%m;
int u=encode(x1,y1,m);
int v=encode(x2,y2,m);
rg[v].push_back(u);
}
}
vector<bool> vis(m*m,false);
queue<int> q;
for(int k=0; k<m; k++)
{
int u1=encode(0,k,m);
if(!vis[u1])
{
vis[u1]=true;
q.push(u1);
}
int u2=encode(k,0,m);
if(!vis[u2])
{
vis[u2]=true;
q.push(u2);
}
}
int cnt=0;
while(!q.empty())
{
int u=q.front(); q.pop();
cnt++;
for(int v:rg[u])if(!vis[v])
{
vis[v]=true;
q.push(v);
}
}
cout << m*m-cnt << endl;
return 0;
}
using ll = long long;
ll A,C,M;
inline ll f(ll x)
{
return (A*x+C)%M;
}
signed main()
{
cin >> A >> C >> M;
// floyd
ll sl=f(0);
ll fa=f(sx); // 快慢指针
while(sl!=fa)
{
sl=f(sl);
fa=f(f(fa));
}
// sx,nx都在环上相遇
// 且i,j到环的入口距离相同
ll t=0; // 预周期
ll i=f(0), j=sl;
// 让i处于x0的位置,j在环入口,不断移动直到相遇
while(i!=j)
{
i=f(i);
j=f(j);
t++;
}
ll T=1; // 周期
j=f(i); // j从环入口下一个结点开始
while(i!=j) j=f(j), T++;
cout << "pre period:" << t << endl;
cout << "period:" << T << endl;
return 0;
}
using ll = long long;
ll A, C, M;
inline ll f(ll x)
{
return (A * x + C) % M;
}
signed main()
{
cin >> A >> C >> M;
vector<ll> dsu(M, -1);
ll x=f(0), s=0;
while(1)
{
if(dsu[x]!=-1)
{
ll t=dsu[x]; // 预周期
ll T=s-dsu[x]; // 周期
cout << t << " " << T << endl;
break;
}
dsu[x]=s;
x=f(x);
s++;
}
return 0;
}
t s
x[0] → x[1] → x[2] → … → x[t-1] → x[t] → x[t+1] → … → x[t+T-1]
↑__________________________|
T = s-t
vector<int> p,ep;
void f(int x)
{
for(int p=2; p*p<=n; p++)if(n%p==0)
{
int c=0;
while(n%p==0) n/=p,c++;
p.push_back(p), ep.push_back(c);
}
if(n>1) p.push_back(n), ep.push_back(1);
}
using ll = long long;
ll mul(ll a, ll b, ll mod)
{
return (__int128)a * b % mod;
}
ll qpow(ll a, ll b, ll mod)
{
ll res=1;
while(b)
{
if(b&1) res=mul(res, a, mod);
a=mul(a, a, mod);
b >>= 1;
}
return res;
}
bool MillerRabin(ll n)
{
if (n < 2) return false;
for (ll p : {2, 3, 5, 7, 11, 13, 17, 19, 23})
{
if (n % p == 0) return n == p;
}
ll d = n - 1, s = 0;
while ((d & 1) == 0)
{
d >>= 1;
s++;
}
auto check = [&](ll a)
{
ll x = qpow(a, d, n);
if (x == 1 || x == n - 1) return true;
for (int i = 1; i < s; i++)
{
x = mul(x, x, n);
if (x == n - 1) return true;
}
return false;
};
for (ll a : {2, 325, 9375, 28178, 450775, 9780504, 1795265022})
{
if (a % n == 0) continue;
if (!check(a)) return false;
}
return true;
}
ll PollardRho(ll n)
{
if(~n&1) return 2;
while(1)
{
ll x=rand()%(n-2)+2;
ll y=x;
ll c=rand()%(n-1)+1;
auto f=[&](ll x)
{
return (mul(x, x, n)+c)%n;
};
ll d=1;
while(d==1)
{
x=f(x);
y=f(f(y));
d=__gcd(abs(x - y), n);
}
if(d!=n) return d;
}
}
map<ll,int> mp; // <p,ep>
// 质因数分解
void factor(ll n)
{
if(n==1) return;
if(MillerRabin(n))
{
mp[n]++;
return;
}
// 用PollardRho分解
ll d=PollardRho(n);
factor(d);
factor(n/d);
}