這個東西看起來好恐怖啊QAQ.jpg搞了幾天,QWQ
其實類歐幾里得演算法,就是和歐幾里得演算法類似(歐幾里得演算法就是gcd那一堆啦),但是其實只有時間複雜度的證明是和gcd一樣的,其它的都完全不同,emmmm。
先前置兩道例題:【luoug-T36097 整點與質數】【[COCI2009-2010#1] ALADIN
】【講解by hdxrie】
其實這個就是一個十分簡單的類歐演算法的應用
以前做到的時候還不知道,現在學了才知道,QAQ
我們可以從一個簡單的式子來入門,並對其套路有一定的瞭解。
T≤105組詢問,每組對於給定的n≤109求下面式子在mod 998244353意義下的值(a,b≥0,c>0):
i=0∑n⌊cai+b⌋
其實前面的⌊qip⌋就是這個式子的簡化版,b=0。
顯然這個不能直接O(n)算,或者除法分塊,所以我們要另尋它路。
首先我們可以分成兩步考慮:
- a≥c或者b≥c時
此時,我們顯然可以將原式寫成:
i=0∑n(⌊ca⌋i+⌊cb⌋+⌊c(a%c)i+(b%c)⌋)
將其拆開可以得到:
⌊ca⌋i=0∑ni+i=0∑n⌊cb⌋+i=0∑n⌊c(a%c)i+(b%c)⌋
前面兩個可以用公式O(1)的求,而後面那個又是一個類似的子問題,遞迴求解即可。
所以我們令f(a,b,c,n)=∑i=0n⌊cai+b⌋,那麼原式可以寫成:
f(a,b,c,n)=⌊ca⌋2n×(n+1)+(n+1)⌊cb⌋+f(a%c,b%c,c,n)
此時我們就把問題從a≥c,b≥c轉換a,b<c的情況了。
- 那麼顯然另外一種情況就是a<c且b<c的時候了。
其實,如果看了hdxrie的關於[COCI2010]ALADIN的那個題的講解,這個也就是相當於求一條直線下整點個數,具體講解可以看這篇論文【洪華敦-類歐幾里得演算法】。
那麼我們可以列舉整點的座標,原式就可以轉化成(這裡m=⌊can+b⌋−1):
f(a,b,c,n)=i=0∑nj=1∑m+1[j⩽⌊cai+b⌋]
這裡[a=1]這種是邏輯判斷符,如果裡面語句為真,那麼值為1,否則為0。
其實這裡就是相當於一條y=cax+b的直線,所以我們列舉i就是列舉的x,j就是y,如果j小於等於的話(i,j)就是在這個直線下的整點。
然後我們先交換列舉順序可以得到:
由於a⩽⌊b⌋時也滿足a⩽b,所以變換時可以把向下取整符號去掉。
f(a,b,c,n)=======j=1∑m+1i=0∑n[j⩽⌊cai+b⌋]j=0∑mi=0∑n[j+1⩽⌊cai+b⌋]j=0∑mi=0∑n[j+1⩽cai+b]j=0∑mi=0∑n[jc+c⩽ai+b]j=0∑mi=0∑n[ai⩾jc+c−b]j=0∑mi=0∑n[ai>jc+c−b−1]j=0∑mi=0∑n[i>⌊ajc+c−b−1⌋]
由於在i∈[0,n]中的i>⌊ajc+c−b−1⌋的只有⌊ajc+c−b−1⌋∼n,所以進而化簡得:
f(a,b,c,n)===j=0∑m(n−⌊ajc+c−b−1⌋)j=0∑mn−j=0∑m⌊ajc+c−b−1⌋ n×(m+1)−f(c,c−b−1,a,m)
那麼這樣又可以將f(a,b,c,n)⇒f(c,c−b−1,a,⌊can+b⌋−1),又將問題變小了。
那麼邊界就是當:
- n=0時,直接返回⌊cb⌋
- a=0時,就是(n+1)×⌊cb⌋
f總結
f(a,b,c,n)=⎩⎪⎪⎪⎨⎪⎪⎪⎧⌊cb⌋(n+1)×⌊cb⌋⌊ca⌋2n×(n+1)+(n+1)⌊cb⌋+f(a%c,b%c,c,n)n×⌊can+b⌋−f(c,c−b−1,a,m)n=0a=0a⩾c or b⩾ca<c and b<c
這時就已經可以寫出一份十分簡單的遞迴的程式碼了:
int S1(int n){return n*(n+1)/2;}
int f(int a,int b,int c,int n){
if(!n) return b/c;
if(!a) return (n+1)*(b/c);
if(a>=c||b>=c)return S1(n)*(a/c)+n*(b/c)+f(a%c,b%c,c,n);
return n*((a*n+b)/c)-f(c,c-b-1,a,(a*n+b)/c-1);
}
關於複雜度,首先我們看a≥c,b≥c的時候只需O(1)就轉移到了a<c,b<c的情況了,而這時f(a,b,c,n)⇒f(c,c−b−1,a,⌊can+b⌋−1)這個就十分類似於歐幾里得演算法的更相減損術,直到a=0或n=0時就停止了,那麼複雜度就類似於歐幾里得演算法,是O(logn)的。
有了初步的瞭解,我們就可以看看稍微難一點的:
同樣的條件,再求下面兩個式子的值:
i=0∑n(⌊cai+b⌋)2
和
i=0∑ni⌊cai+b⌋
之所以放在一起說,是因為這兩個在求取的過程中會有聯絡,而且還要用到前面的f。
首先我們令h(a,b,c,n)=∑i=0n(⌊cai+b⌋)2,g(a,b,c,n)=∑i=0ni⌊cai+b⌋
那麼我們先來看h的求法,和f類似(其實類歐的套路都差不多是這樣),我們分兩塊考慮:
- 當a≥c或者b≥c時:
顯然同理有:
h(a,b,c,n)=i=0∑n(⌊ca⌋i+⌊cb⌋+⌊c(a%c)i+(b%c)⌋)2
我們把那個恐怖的括號開啟,可以得到((a+b+c)2=a2+b2+c2+2ab+2bc+2ac):
h(a,b,c,n)=i=0∑n(⌊ca⌋i)2+i=0∑n(⌊cb⌋)2+i=0∑n(⌊c(a%c)i+(b%c)⌋)2+i=0∑n2⋅⌊ca⌋i⋅⌊cb⌋+i=0∑n2⋅⌊cb⌋⋅⌊c(a%c)i+(b%c)⌋+i=0∑n2⋅⌊ca⌋i⋅⌊c(a%c)i+(b%c)⌋
我們把這一大坨,整理化簡一下,可以得到:
由於∑i=0ni2=6n×(n+1)×(2n+1),∑i=0ni=2n×(n+1)
h(a,b,c,n)=(⌊ca⌋)26n×(n+1)×(2n+1)+(n+1)×(⌊cb⌋)2+h(a%c,b%c,c,n)+2⋅⌊ca⌋⋅⌊cb⌋⋅2n×(n+1)+2⋅⌊cb⌋⋅f(a%c,b%c,c,n)+2⋅⌊ca⌋⋅g(a%c,b%c,c,n)
遞迴處理就可以了(假設我們現在知道g如何計算)。
- 我們繼續考慮當a<c,b<c的時候的情況:
通過f的經驗,我們照著之前的變換一下看(m的意義同前面):
h(a,b,c,n)=i=0∑n(j=1∑m+1[j⩽⌊cai+b⌋])2
我們將後面拆成兩個式子,然後按照f的方式變換:
h(a,b,c,n)=====i=0∑n(j=1∑m+1[j⩽⌊cai+b⌋])×(k=1∑m+1[k⩽⌊cai+b⌋])i=0∑n(j=0∑m[j+1⩽cai+b])×(k=0∑m[k+1⩽cai+b])i=0∑n(j=0∑m[jc+c⩽ai+b])×(k=0∑m[kc+c⩽ai+b])i=0∑n(j=0∑m[ai⩾jc+c−b])×(k=0∑m[ai⩾kc+c−b])i=0∑n(j=0∑m[ai>jc+c−b−1])×(k=0∑m[ai>kc+c−b−1])
然後我們同理,交換列舉順序,可以得到:
h(a,b,c,n)==j=0∑mk=0∑mi=0∑n[ai>max(jc−b−1,kc−b−1)]j=0∑mk=0∑mi=0∑n[i>max(⌊ajc+c−b−1⌋,⌊akc+c−b−1⌋)]
同樣,i∈[0,n]的i>max(⌊ajc+c−b−1⌋,⌊akc+c−b−1⌋)的只有>max(⌊ajc+c−b−1⌋,⌊akc+c−b−1⌋)∼n這一部分,所以原式還可以寫成:
h(a,b,c,n)==j=0∑mk=0∑m(n−max(⌊ajc+c−b−1⌋,⌊akc+c−b−1⌋))n(m+1)2−j=0∑mk=0∑mmax(⌊ajc+c−b−1⌋,⌊akc+c−b−1⌋)
這裡的max不好處理,但是我們可以通過列舉當前這個是max的貢獻,首先j⩾k的部分為:
j=0∑m(j+1)⌊ajc+c−b−1⌋
因為對於每個j,都有0∼j也就是j+1個k比它小,所以每個的貢獻次數是j+1次。
那麼同理對於j⩽k的部分就是:
k=0∑m(k+1)⌊akc+c−b−1⌋
但是這兩個加起來,會把j=k的部分多加一次,所以我們減去多加了的這個部分:
j=0∑m⌊ajc+c−b−1⌋
那麼原式就可以變成:
h(a,b,c,n)===n(m+1)2−j=0∑mk=0∑mmax(⌊ajc+c−b−1⌋,⌊akc+c−b−1⌋)n(m+1)2−(j=0∑m(j+1)⌊ajc+c−b−1⌋+k=0∑m(k+1)⌊akc+c−b−1⌋−j=0∑m⌊ajc+c−b−1⌋)n(m+1)2−(2⋅j=0∑m(j+1)⌊ajc+c−b−1⌋−j=0∑m⌊ajc+c−b−1⌋)
我們把(j+1)開啟,然後整理可以得到:
h(a,b,c,n)===n(m+1)2−(2⋅j=0∑mj⌊ajc+c−b−1⌋+2⋅j=0∑m⌊ajc+c−b−1⌋−j=0∑m⌊ajc+c−b−1⌋)n(m+1)2−(2⋅j=0∑mj⌊ajc+c−b−1⌋+j=0∑m⌊ajc+c−b−1⌋)n(m+1)2−2⋅g(c,c−b−1,a,m)−f(c,c−b−1,a,m)
那麼我們同樣可以將其轉移到更小的問題上,遞迴計算即可。
邊界同理:
- 當n=0時,就是(cb)2
- 當a=0時,就是(n+1)×(cb)2
h總結
h(a,b,c,n)=⎩⎪⎪⎪⎪⎨⎪⎪⎪⎪⎧(cb)2(n+1)×(cb)2(⌊ca⌋)26n(n+1)(2n+1)+(n+1)(⌊cb⌋)2+h(a%c,b%c,c,n)+2⌊ca⌋⌊cb⌋2n(n+1)+2⌊cb⌋f(a%c,b%c,c,n)+2⌊ca⌋g(a%c,b%c,c,n)n(m+1)2−2⋅g(c,c−b−1,a,m)−f(c,c−b−1,a,m)n=0a=0a⩾c or b⩾ca<c and b<c
程式碼同樣十分好寫,仿照上面的即可,這裡就不寫了。
接下來只剩下對於g的求法了。
g相對h來說就很好變換了,直接跟著套路走:
- 當a⩾c,b⩾c時:
g(a,b,c,n)===i=0∑ni⌊cai+b⌋i=0∑n⌊ca⌋i2+⌊cb⌋i+i⌊c(a%c)i+(b%c)⌋⌊ca⌋6n(n+1)(2n+1)+⌊cb⌋2n(n+1)+g(a%c,b%c,c,n)
- 當a<c,b<c
根據套路,我們轉換成這個形式:
g(a,b,c,n)======j=0∑mi=0∑ni[i>⌊ajc+c−b−1⌋]j=0∑mi=⌊ajc+c−b−1⌋+1∑nij=0∑m⎝⎜⎛i=0∑ni−i=0∑⌊ajc+c−b−1⌋i⎠⎟⎞j=0∑m2n(n+1)−2⌊ajc+c−b−1⌋⋅(⌊ajc+c−b−1⌋+1)21(n(n+1)(m+1)−j=0∑m(⌊ajc+c−b−1⌋)2−⌊ajc+c−b−1⌋)21(n(n+1)(m+1)−h(c,c−b−1,a,m)−f(c,c−b−1,a,m))
由此我們可以直接判斷後遞迴計算即可。
邊界條件還是一樣的:
- 當n=0時,為0。
- 當a=0時,為⌊cb⌋(2n(n+1))
g總結
g(a,b,c,n)=⎩⎪⎪⎪⎨⎪⎪⎪⎧0⌊cb⌋(2n(n+1))⌊ca⌋6n(n+1)(2n+1)+⌊cb⌋2n(n+1)+g(a%c,b%c,c,n)21(n(n+1)(m+1)−h(c,c−b−1,a,m)−f(c,c−b−1,a,m))n=0a=0a⩾c or b⩾ca<c and b<c
程式碼同理。
技巧:為了方便求取g,h,通常我們可以在一個遞迴裡面寫,可以降低複雜度,並且寫起了也挺簡單的。
這個算是個簡單的模板了,求的就是上面講的f,h,g函式:
#include<cstdio>
#include<cstring>
#include<algorithm>
#define ll long long
using namespace std;
const ll Mod=998244353;
const ll inv2=499122177,inv6=166374059;
ll S1(ll x){if(x>=Mod)x%=Mod;return (x*(x+1)%Mod)*inv2%Mod;}
ll S2(ll x){if(x>=Mod)x%=Mod;return (x*(x+1)%Mod*(x+x+1)%Mod)*inv6%Mod;}
ll Sqr(ll x){return x*x%Mod;}
struct node{
ll f,g,h;
void clear(){f=g=h=0;}
node(){}
node(ll a,ll b,ll c):f(a),g(b),h(c){}
void out(){printf("%lld %lld %lld\n",f,g,h);}
};
node calc(ll a,ll b,ll c,ll n){
node ans,res;ans.clear();
ll m,t1,t2,s1,s2;
if(!n){ans.f=b/c;ans.g=Sqr(b/c);return ans;}
if(!a){
t1=b/c;
ans.f=(n+1ll)*t1%Mod;
ans.g=(n+1ll)*Sqr(t1)%Mod;
ans.h=S1(n)*t1%Mod;
return ans;
}
if(a>=c||b>=c){
t1=a/c;t2=b/c;
res=calc(a%c,b%c,c,n);
s1=S1(n);s2=S2(n);
ans.f=(((s1*t1%Mod)+(n+1ll)*t2%Mod)%Mod+res.f)%Mod;
ans.g=(((Sqr(t1)*s2%Mod+(n+1ll)*Sqr(t2)%Mod)%Mod)+((t1*t2%Mod)*2ll*s1%Mod+(t1*2ll*res.h%Mod))%Mod+(res.g+t2*2ll*res.f%Mod)%Mod)%Mod;
ans.h=((s2*t1%Mod+s1*t2%Mod)+res.h)%Mod;
return ans;
}
m=(n*a+b)/c-1;
res=calc(c,c-b-1,a,m);
ll w1=n*(m+1)%Mod,w2=n*(n+1)%Mod,w3=m+1;
if(w3>=Mod)w3%=Mod;
ans.f=(w1-res.f)%Mod;if(ans.f<0)ans.f+=Mod;
ans.g=((w1*w3)%Mod-((res.h*2ll%Mod+res.f)%Mod))%Mod;if(ans.g<0)ans.g+=Mod;
ans.h=((w2*w3)%Mod-(res.f+res.g)%Mod)%Mod*inv2%Mod;if(ans.h<0)ans.h+=Mod;
return ans;
}
int a,b,c,n,T;
int main(){
for(scanf("%d",&T);T--;){scanf("%d%d%d%d",&n,&a,&b,&c);calc(a,b,c,n).out();}
return 0;
}
我們其實可以求取更加一般化的式子,如這道模板題【IN-loj】
題意就是多組詢問,給定k1,k2,a,b,c,n求下面式子在mod 998244353意義下的值。
i=0∑nik1⌊cai+b⌋k2
資料範圍見原題面。
- 當k1=0,k2=1時,就是f函式。
- 當k1=0,k2=2時,就是h函式。
- 當k1=1,k2=1時,就是g函式。
那麼當k1,k2不為上面的那些情況的時候應該怎麼辦呢?
我們還是按照套路來分析,我們令F(k1,k2,a,b,c,n)=∑i=0nik1⌊cai+b⌋k2:
- 當a≥c,b≥c時:
F(k1,k2,a,b,c,n)=i=0∑nik1(⌊ca⌋i+⌊cb⌋+⌊c(a%c)i+(b%c)⌋)k2
用二項式定理,我們可以將後面的括號展開得到:
用二項式定理推導一下可以得到
(a+b+c)n=∑i+j+k=nCniCn−ijaibjck
F(k1,k2,a,b,c,n)=I=0∑ni+j+k=k2∑Ck2iCk2−ij⌊ca⌋i⌊cb⌋j(Ik1+i)⌊c(a%c)I+(b%c)⌋k=i+j+k=k2∑Ck2iCk2−ij⌊ca⌋i⌊cb⌋jI=0∑n(Ik1+i)⌊c(a%c)I+(b%c)⌋k0≤i,j,k
我們可以發現,最後面那堆等於F(k1+i,k,a%c,b%c,c,n),所以遞迴處理即可。
- 當a<c,b<c的時候:
我們知道xk=∑i=0x−1(i+1)k−ik
因為其等於:xk−(x−1)k+(x−1)k−(x−2)k+⋯−0k=xk−0=xk
那麼我們用其來變換原式得到:
F(k1,k2,a,b,c,n)=i=0∑nik1j=0∑⌊cai+b⌋−1(j+1)k2−jk2
我們交換列舉順序可以得到(m=⌊can+b⌋−1):
F(k1,k2,a,b,c,n)===j=0∑m((j+1)k2−jk2)i=0∑nik1[j≤⌊cai+b⌋]j=0∑m((j+1)k2−jk2)i=0∑nik1[i<⌊ajc+c−b−1⌋]j=0∑m((j+1)k2−jk2)⎝⎛i=0∑nik1−i=0∑⌊ajc+c−b−1⌋ik1⎠⎞
我們接下來將前面那一堆(也就是相當於(m+1)k2)乘進後面,得到:
F(k1,k2,a,b,c,n)==j=0∑m((j+1)k2−jk2)i=0∑nik1−j=0∑m((j+1)k2−jk2)i=0∑⌊ajc+c−b−1⌋ik1(m+1)k2i=0∑nik1−j=0∑m((j+1)k2−jk2)i=0∑⌊ajc+c−b−1⌋ik1
而我們知道,關於自然數冪的字首和是一個關於冪次+1的多項式,所以我們可以用拉格朗日插值(或者高斯消元)把其係數求出來,然後就可以帶入到後面那一堆,再用二項式定理展開,得到:
假設∑i=0k+1ak,ixi=∑i=0xik
F(k1,k2,a,b,c,n)=====(m+1)k2i=0∑nik1−j=0∑m((j+1)k2−jk2)i=0∑⌊ajc+c−b−1⌋ik1(m+1)k2i=0∑nik1−k=0∑k2−1⎝⎛j=0∑mCk2kjk⋅i=0∑⌊ajc+c−b−1⌋ik1⎠⎞(m+1)k2i=0∑nik1−k=0∑k2−1(j=0∑mCk2kjk⋅i=0∑k1+1ak1,i(⌊ajc+c−b−1⌋)i)(m+1)k2i=0∑nik1−k=0∑k2−1i=0∑k1+1Ck2kak1,i⋅j=0∑mjk(⌊ajc+c−b−1⌋)i(m+1)k2i=0∑nik1−k=0∑k2−1i=0∑k1+1Ck2kak1,i⋅F(k,i,c,c−b−1,a,m)
其中,有一步是這樣變換的:
i=0∑x−1(i+1)k−ik=i=0∑x−1j=0∑kCkjij−ik=i=0∑x−1j=0∑k−1Ckjij=j=0∑k−1(i=0∑x−1Ckjij)
那麼,這個和前面的其實一樣,遞迴計算就好啦,只不過要提前預處理一下係數和組合數。
k1,k2比較小,所以每次算的時候暴力列舉一下(或者可以用FFT之類的優化一下)就行了。
#include<cstdio>
#include<cstring>
#include<algorithm>
#define ll long long
using namespace std;
const int M=13;
const ll Mod=1e9+7;
ll y[M],C[M][M],F[M][M][44];
ll fpow(ll a,ll b=Mod-2){
a%=Mod;if(a<0)a+=Mod;ll res=1;for(;b;b>>=1,a=(a*a)%Mod)if(b&1)res=(res*a)%Mod;
return res;
}
struct Ply{
ll x[M];
void clear(){memset(x,0,sizeof(x));}
Ply(){memset(x,0,sizeof(x));}
Ply operator +(const Ply &a)const{
Ply ans;for(int i=0;i<M;i++)ans.x[i]=(x[i]+a.x[i])%Mod;
return ans;
}
Ply operator *(const Ply &a)const{
Ply ans;
for(int i=0;i<M;i++)for(int j=0;i+j<M;j++)
ans.x[i+j]=(ans.x[i+j]+x[i]*a.x[j]%Mod)%Mod;
return ans;
}
ll operator ()(ll a,int b){
ll ans=x[b+1];for(int i=b;i>=0;i--)ans=(ans*a%Mod+x[i])%Mod;
return ans;
}
}A[M],B[M];
void calc(Ply &a,int n){
Ply xx,yy;
for(int i=0;i<=n;i++){
xx.x[0]=y[i];
for(int j=0;j<=n;j++)if(i^j)xx.x[0]=xx.x[0]*fpow(i-j)%Mod;
for(int j=0;j<=n;j++)if(j^i){yy.x[0]=Mod-j;yy.x[1]=1;xx=xx*yy;}
a=a+xx;xx.clear();
}
}
ll f(int k1,int k2,ll a,ll b,ll c,ll n,int d=0){
if(~F[k1][k2][d]) return F[k1][k2][d];
ll ans=0;
if(!a) ans=B[k1](n,k1)*fpow(b/c,k2)%Mod;
else if(!k2) ans=B[k1](n,k1);
else if(a>=c||b>=c){
int x=a/c,y=b/c;a%=c;b%=c;
for(ll i=0,X=1;i<=k2;i++,X=(X*x)%Mod){
for(ll j=0,Y=X;i+j<=k2;j++,Y=Y*y%Mod){
ans=(ans+f(k1+i,k2-i-j,a,b,c,n,d+1)*C[k2][i]%Mod*C[k2-i][j]%Mod*Y%Mod)%Mod;
}
}
}else{
int m=(a*n+b)/c;--m;
ans=A[k2](m,k2);
ans=ans*B[k1](n,k1)%Mod;
for(int j=0;j<k2;j++)for(int i=0;i<=k1+1;i++){
ans=(ans-f(j,i,c,c-b-1,a,m,d+1)*C[k2][j]%Mod*B[k1].x[i]%Mod)%Mod;
}
}
if(ans<0)(ans%=Mod)+=Mod;ans%=Mod;
return F[k1][k2][d]=ans;
}
void init(){
C[0][0]=1;
for(int i=1;i<=10;i++){
C[i][0]=C[i][i]=1;
for(int j=1;j<i;j++){
C[i][j]=(C[i-1][j]+C[i-1][j-1])%Mod;
}
}
for(int i=0;i<=10;i++){
if(i)y[0]=1;
for(int j=1;j<=i+1;j++){
y[j]=(y[j-1]+fpow(j+1,i)-fpow(j,i))%Mod;
if(y[j]<0)y[j]+=Mod;
}
calc(A[i],i+1);
if(!i)y[0]=1;else y[0]=0;
for(int j=1;j<=i+1;j++)y[j]=(y[j-1]+fpow(j,i))%Mod;
calc(B[i],i+1);
}
}
int T,a,b,c,n,k1,k2;
int main(){
init();
for(scanf("%d",&T);T--;){
scanf("%d%d%d%d%d%d",&n,&a,&b,&c,&k1,&k2);
memset(F,-1,sizeof(F));
printf("%d\n",f(k1,k2,a,b,c,n));
}
return 0;
}
題目:
LOJ模板題的另外兩個講解:
End