「算法筆記」組合入門與應用


相關內容:組合入門題目選做(應用在這兒呢,可配合該文章閱讀  )

一、基礎內容

(這部分內容大家應該都會了,可以直接跳過)

1. 一些定義

加法原理:一般地,做一件事,完成它可以有 \(n\) 類方法,在第一類辦法中有 \(m_1\) 種不同的方法,在第二類辦法中有 \(m_2\) 種不同的方法,……,在第 \(n\) 類辦法中有 \(m_n\) 種不同的方法, 那么完成這件事共有:\(N=m_1+m_2+...+m_n\) 種不同的方法。

比如在一個問題中,路徑之間是平行的關系,你可以選擇任意一條路徑到達終點,這些路徑就是滿足加法原理的一個關系。

乘法原理:一般地,做一件事,完成它需要分成 \(n\) 個步驟,做完第一步有 \(m_1\) 種不同的方法,做完第二步有 \(m_2\) 種不同的方法,……,做完第 \(n\) 步有 \(m_n\) 種不同的方法。 那么,完成這件事共有:\(N=m_1\times m_2\times ...\times m_n\) 種不同的方法。

這個應該很好懂。注意區分“方法”與“步驟”。

排列:\(n\) 個不同元素重新排列,方案數為:\(n!\)

\(n\) 個不同元素中選出 \(m\) 個元素排成一列,產生的不同排列的數量為:

\(A_n^m=\frac{n!}{(n-m)!}=n\times (n-1)\times ...\times (n-m+1)\)(也可記作 \(P_n^m\)

組合:\(n\) 個不同元素中選出 \(m\) 個組成一個集合(不考慮順序)的方案數。

\(C_n^m=\frac{n!}{m!(n-m)!}=\frac{n\times (n-1)\times ...\times (n-m+1)}{m\times (m-1)\times ...\times 2\times 1}\)

\(C_n^m\) 也記作 \(\binom{n}{m}\)

2. 一些理解

怎么理解 \(C_n^m=\frac{n!}{m!(n-m)!}\) 呢?一個長度為 \(n\) 的排列,選取其中的 \(m\) 個元素。首先,我們已經知道,把 \(n\) 個不同元素重新排列,有 \(n!\) 種方案。那么,我們用總的方案數去掉重復的方案數就是答案。假設我們已經確定了一個選出的集合為 \(S\)\(|S|=m\)),並且這個集合對應排列的前 \(m\) 個元素(順序可以打亂)。對於序列的前 \(m\) 個位置(即集合 \(S\) 中的每個元素),有 \(m!\) 種排列方法。對於序列的后 \(n-m\) 個元素,有 \((n-m)!\) 種排列方法。兩者滿足乘法原理,則有 \(m!\cdot (n-m)!\) 種排列。所以如果確定了集合 \(S\),則對於這個集合 \(S\),有 \(m!\cdot (n-m)!\) 種方案。可以看成是,每 \(m!\cdot (n-m)!\) 種方案,才能確定一個集合 \(S\)。那么產生不同集合 \(S\) 的數量為 \(\frac{n!}{m!(n-m)!}\)

既然知道了 \(C_n^m=\frac{n!}{m!(n-m)!}\),那么 \(A_n^m=\frac{n!}{(n-m)!}\) 也非常好理解了。對於 \(A_n^m\),從 \(n\) 個不同元素中選出的 \(m\) 個元素可以重新排列。選出的 \(m\) 個元素有 \(m!\) 種排列方法,那么 \(A_n^m=C_n^m\times m!=\frac{n!}{m!(n-m)!}\times m=\frac{n!}{(n-m)!}\)

二、組合數的求法

1. 當數據范圍小:利用 \(C_n^m=\frac{n!}{m!(n-m)!}\) 直接暴力計算。\(O(n)\) 的計算 \(n!\)

2. \(n,m\leq 1000\)。用遞推法(楊輝三角)。\(C_{i,j}=C_{i-1,j}+C_{i-1,j-1}\)

c[0][0]=1;
for(int i=1;i<=n;i++){
    c[i][0]=1;
    for(int j=1;j<=i;j++)
        c[i][j]=(c[i-1][j]+c[i-1][j-1])%mod;
} 

3. \(n,m\leq 10^5,10^6\),求 \(C_n^m \bmod p\)

分別計算分子與分母,然后相除。除以 \(m!(n-m)!\) 可以看成是乘以 \(m!(n-m)! \bmod p\) 的逆元。

預處理出 \(i!\)\(\frac{1}{i!}\)\(i! \bmod p\) 的逆元)。

預處理 \(i!\):用 \(fac_i\) 表示 \(i!\)\(fac_0=1\),對於 \(\forall i>0\)\(fac_i=fac_{i-1}\times i\)

預處理 \(\frac{1}{i!}\):用 \(inv_i\) 表示 \(i! \pmod p\) 的逆元,則 \(inv_m=mul(fac_m,p-2,p)\)。先 \(O(log)\) 計算 \(inv_m\)\(\frac{1}{i!}=\frac{1}{1\times 2\times ...\times i\times (i+1)}\times (i+1)=\frac{1}{(i+1)!}\times (i+1)\),那么 \(inv_i=inv_{i+1}\times (i+1)\)

//快速冪: 
int mul(int x,int n,int mod){
    int ans=mod!=1;
    for(x%=mod;n;n>>=1,x=x*x%mod)
        if(n&1) ans=ans*x%mod;
    return ans;
}
//對階乘數及其逆元進行預處理:
void init(){ 
    f[0]=g[0]=1;
    for(int i=1;i<=n;i++)
        f[i]=f[i-1]*i%mod;    //f(i) 表示 i! ,即 fac(i)
    g[n]=mul(f[n],mod-2,mod);    //求逆元 
    for(int i=n-1;i;i--)
        g[i]=g[i+1]*(i+1)%mod;     //g(i) 表示 1/(i!) ,即 inv(i)
} 
//之后就可以調用:
int solve(int n,int m){
    return f[n]*g[m]%mod*g[n-m]%mod;
} 

4. 若題目中 \(n\) 非常大,甚至沒法用數組存下,而 \(m\) 非常小,則可以直接計算。

\(C_n^m=\frac{n!}{m!(n-m)!}\) ,因為 \(m\) 非常小,所以 \(m!\) 可以很快被計算出來。

再考慮 \(\frac{n!}{(n-m)!}\)\(\frac{n!}{(n-m)!}=\frac{n\times (n-1)\times ...\times (n-m+1)\times (n-m)\times (n-m-1)\times ...\times 2\times 1}{(n-m)\times (n-m-1)\times ...\times 2\times 1}=n\times (n-1)\times ...\times (n-m+1)\)。只有 \(m\) 項。也可以暴力計算。

三、Catalan 數

吹一波 Dls 寫的,比這里寫的不知道高到哪里去了。

1. 卡特蘭數的定義

給定 \(n\)\(0\)\(n\)\(1\),它們按照某種順序排成長度為 \(2n\) 的序列,滿足任意前綴中 \(0\) 的個數都不少於 \(1\) 的個數的序列的數量為:

\(Cat_n=\frac{1}{n+1} C_{2n}^n=\frac{(2n)!}{(n+1)!n!}\)

2. 證明

為什么 \(Cat_n=\frac{1}{n+1} C_{2n}^n\) 呢?

考慮取補集。

首先,有 \(n\)\(0\)\(n\)\(1\) 的序列的數量為 \(C_{2n}^n\)\(2n\) 個位置中取 \(n\) 個位置為 \(1\))。

\(n\)\(0\)\(n\)\(1\) 隨意排成一個長度為 \(2n\) 的序列,若它不滿足任意前綴中 \(0\) 的個數都不少於 \(1\) 的個數,找到它的第一個不合法的位置(不合法指前綴中 \(0\) 的個數小於了 \(1\) 的個數)。顯然,第一個不合法位置上的數一定是 \(1\),並且在它的前面,\(0\) 的個數與 \(1\) 的個數相等。假設有 \(i\) 個。那么這個位置(即第一個不合法的位置)后面肯定有 \(n-i\)\(0\)\(n-i-1\)\(1\)。考慮把前 \(2\times i+1\) 位(即不合法位置以及它前面的數)翻轉(\(0\) 變為 \(1\)\(1\) 變為 \(0\))。翻轉后,第一個不合法位置上的數為 \(0\),它的前面依然是 \(i\)\(0\)\(i\)\(1\),它的后面還是 \(n-i\)\(0\)\(n-i-1\)\(1\)。則翻轉后,序列共有 \(n+1\)\(0\)\(n-1\)\(1\)。對於一個不合法的序列,它總是能夠翻轉得到一個有 \(n+1\)\(0\)\(n-1\)\(1\) 的序列。那么我們接下來要證明的就是,對於所有這樣的序列,總是能夠找到一個不合法的序列與它對應。

類似地,在翻轉后的序列中,找到第一個不滿足前綴 \(1\) 的個數大於等於前綴 \(0\) 的個數的數的位置。很容易發現,在翻轉后的序列中,總是能找到這樣一個不合法的位置(因為有 \(n+1\)\(0\)\(n-1\)\(1\))。將這第一個不合法位置以及它前面的數翻轉回去,可以得到一個有 \(n\)\(0\)\(n\)\(1\) 的序列,並且存在前綴 \(0\) 的個數大於前綴 \(1\) 的個數。

所以,不合法序列將不合法位置翻轉,可以和一個 \(n-1\)\(0\)\(n+1\)\(1\) 的序列一一對應。那么,求不合法序列的數量就可以轉化為求有 \(n+1\)\(0\)\(n-1\)\(1\) 的序列的數量。有 \(n+1\)\(0\)\(n-1\)\(1\) 的序列的數量顯然為 \(C_{2n}^{n-1}\)\(2n\) 個位置中取 \(n-1\) 個位置為 \(1\))。

所以合法序列的答案為:\(C_{2n}^n-C_{2n}^{n-1}=\frac{(2n)!}{n!n!}-\frac{(2n)!}{(n-1)!(n+1)!}=\frac{(2n)!(n+1)}{n!(n+1)!}-\frac{(2n)!n}{n!(n+1)!}=\frac{(2n)!}{n!n!(n+1)}=\frac{1}{n+1}\times \frac{(2n)!}{n!n!}=\frac{1}{n+1} C_{2n}^n\)

四、錯排

考慮一個有 \(n\) 個元素的排列,若一個排列中所有的元素都不在自己原來的位置上,那么這樣的排列就稱為原排列的一個錯排。

\(D_n\) 表示 \(n\) 個元素的錯排方案數。

錯排公式:\(D_n=(n-1)\times (D_{n-1}+D_{n-2})\)\(n>2\)

如何理解?考慮序列中 \(1\) 元素的位置。\(1\) 元素能放的位置為 \(2,3,...,n\),有 \(n-1\) 種放的位置。

假設 \(1\) 放在了 \(k\) 這個位置。接下來我們要關心的是 \(k\) 要放在什么位置。有兩種情況。

  • \(k\) 放在 \(1\) 的位置。相當於 \(1\)\(k\) 交換了位置,對剩下的數不會產生影響(剩下的 \(n-2\) 個數依然保持一一對應關系),則剩下的 \(n-2\) 個數可以繼續進行錯排。所以這種情況的方案數為 \((n-1)\times D_{n-2}\)

  • \(k\) 不放在 \(1\) 的位置。\(1\) 的位置確定了,剩下的數(除了 \(k\))依然有自己的對應關系。新增一個對應關系 \((1,k)\) 表示 \(k\) 不能放在 \(1\) 的位置。問題就轉化為了求 \(n-1\) 個數的錯排方案(想一想錯排的定義,\(k\) 與剩下的數每個數都不能在特定的位置上,其實就是錯排)。所以這種情況的錯排方案數為 \((n-1)\times D_{n-1}\)

於是可以得到 \(D_n=(n-1)\times (D_{n-1}+D_{n-2})\)\(n>2\))。

d[0]=1,d[1]=0,d[2]=1;
for(int i=3;i<=1e6;i++)
    d[i]=(i-1)*(d[i-1]+d[i-2])%mod;     //錯排 

五、Lucas 定理

\(p\) 是質數,則對於任意整數 \(1\leq m\leq n\),有:

\(C_n^m\equiv C_{n \bmod p}^{m \bmod p}\times C_{n/p}^{m/p} \pmod p\)

也就是把 \(n\)\(m\) 表示成 \(p\) 進制數,對 \(p\) 進制下的每一位分別計算組合數,最后再乘起來。

模板題

#include<bits/stdc++.h>
#define int long long
const int N=1e5+5;
int t,n,m,p;
int mul(int x,int n,int mod){
    int ans=mod!=1;
    for(x%=mod;n;n>>=1,x=x*x%mod)
        if(n&1) ans=ans*x%mod;
    return ans;
}
int C(int n,int m){
    if(m>n) return 0;
    int x=1,y=1;
    for(int i=n-m+1;i<=n;i++) x=x*i%p;
    for(int i=2;i<=m;i++) y=y*i%p;
    return x*mul(y,p-2,p)%p;
}
int lucas(int n,int m){
    if(!m) return 1;
    return C(n%p,m%p)*lucas(n/p,m/p)%p;
}
signed main(){
    scanf("%lld",&t);
    while(t--){
        scanf("%lld%lld%lld",&n,&m,&p);
        printf("%lld\n",lucas(n,m));
    }
    return 0;
}

若 \(p\) 不是質數,可以用擴展 Lucas 定理。在這里就不講了,推薦閱讀:擴展盧卡斯詳解

模板題

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e6+5;
int n,m,p,cnt,a[N],b[N];
int mul(int x,int n,int mod){
    int ans=mod!=1;
    for(x%=mod;n;n>>=1,x=x*x%mod)
        if(n&1) ans=ans*x%mod;
    return ans;
}
int exgcd(int a,int b,int &x,int &y){
    if(!b) return x=1,y=0,a;
    int d=exgcd(b,a%b,x,y);
    int z=x; x=y,y=z-y*(a/b);
    return d;
} 
int inv(int n,int mod){
    int x,y;
    exgcd(n,mod,x,y);
    return (x%mod+mod)%mod;
}
int fac(int n,int p,int k){    //k=p^x
    if(!n) return 1;
    int ans=1;
    for(int i=2;i<=k;i++)
        if(i%p!=0) ans=ans*i%k;
    ans=mul(ans,n/k,k);
    for(int i=2;i<=n%k;i++)
        if(i%p!=0) ans=ans*i%k;
    return ans*fac(n/p,p,k)%k;
} 
int C(int n,int m,int p,int k){
    if(n<m) return 0;
    int a=fac(n,p,k),b=fac(m,p,k),c=fac(n-m,p,k),cnt=0;
    for(int i=p;i<=n;i*=p) cnt+=n/i;
    for(int i=p;i<=m;i*=p) cnt-=m/i;
    for(int i=p;i<=n-m;i*=p) cnt-=(n-m)/i;
    return a*inv(b,k)%k*inv(c,k)%k*mul(p,cnt,k)%k;
} 
int CRT(int m){
    int ans=0;
    for(int i=1;i<=cnt;i++){
        int M=m/b[i],t=inv(M,b[i]);
        ans=(ans+a[i]%m*M%m*t%m)%m; 
    } 
    return ans;
}
int exLucas(int n,int m,int p){
    int t=p;
    for(int i=2;i*i<=p;i++){ 
        int now=1;
        if(t%i==0){
            b[++cnt]=i;
            while(t%i==0) t=t/i,now*=i;
            a[cnt]=C(n,m,i,now),b[cnt]=now;
        }
    } 
    if(t!=1) b[++cnt]=t,a[cnt]=C(n,m,t,t);
    return CRT(p);
}
signed main(){
    scanf("%lld%lld%lld",&n,&m,&p);
    printf("%lld\n",exLucas(n,m,p));
    return 0;
}

六、容斥原理

\(\left|\bigcup\limits_{i=1}^{n}A_i\right|=\sum\limits_{i=1}^n\left| A_i \right|-\sum\limits_{1\leq i<j\leq n}\left|A_i\cap A_j\right|+\sum\limits_{1\leq i<j<k\leq n}\left|A_i\cap A_j\cap A_k\right|-\cdots+(-1)^n\left| A_1\cap \cdots \cap A_n\right|\)

\(\left|\bigcup\limits_{i=1}^{n}A_i\right|=\sum\limits_{k=1}^n (-1)^{k+1}(\sum\limits_{1\leq i_1<\cdot<i_k\leq n}\left|A_{i_1}\cap\cdots\cap A_{i_k}\right|)\)

\(\left|\bigcup\limits_{i=1}^{n}A_i\right|=\sum\limits_{\emptyset\neq J\subseteq\{1,2,...,n\}} (-1)^{\left|J\right|-1}\left|\bigcap\limits_{j\in J}A_j\right|\)

常見應用:題中給出 \(n\) 個需要滿足的條件。

使用容斥,枚舉不被滿足的條件集合 \(S\) 和對應的方案數,其系數為 \((-1)^{集合大小}\)。

具體地,設 \(A\) 為條件集合。式子:

\(\sum\limits_{S\in A}(-1)^{\left| S\right|}\cdot f(S中的條件不被滿足)\)

七、第二類斯特林數

\(n\) 個不同的球放到 \(r\) 個相同的盒子里,假設沒有空盒,則放球方案數記做 \(S(n,r)\)\(\begin{Bmatrix}n\\r\end{Bmatrix}\),稱為第二類 \(\text{Stirling}\) 數。

1. 求法

有一個顯然的遞推式:

\(S(n,r)=rS(n-1,r)+S(n-1,r-1),n>r\geq 1\)

即討論當前的球是放入以前的盒子還是放入一個新盒子里。如果要放入以前的盒子,那么把這個球放入任意一個盒子,這個盒子就相當於與其他的盒子不同,所以還要乘以 \(r\)

同樣有通項:

\(\begin{Bmatrix}n\\k\end{Bmatrix}=\dfrac{1}{k!}\displaystyle\sum\limits_{i=0}^k (-1)^i \dbinom{k}{i} (k-i)^n\)

注意到“盒子非空”可以作為容斥的一個條件,\(k\) 個盒子就對應 \(k\) 個條件。枚舉空盒的個數 \(i\)\(k\) 個盒子選 \(i\) 個盒子是空盒的方案數為 \(C_k^i\)。確定了有 \(i\) 盒子是空盒,那么剩下 \(k-i\) 個盒子是可以放球的,則每個球都有 \(k-i\) 個選擇方式(可以放到 \(k-i\) 個盒子中的任意一個里),所以要乘上 \((k-i)^n\)。由於盒子是相同的最后要除以 \(k!\)

2. 性質

第二類 \(\text{Stirling}\) 數的性質:

\(x^n=\displaystyle\sum\limits_{k=0}^n\begin{Bmatrix}n\\k\end{Bmatrix} x^{\underline k}\)

其中 \(x^{\underline k}\) 表示 \(x\)\(k\) 次下降冪,\(x^{\underline k}=\frac{x!}{(x-k)!}\)

怎么理解呢?\(x^n\)\(n\) 個球放入 \(x\) 個不相同的盒子的方案數(所以也可以有空盒)。枚舉非空盒子的個數 \(k\)\(x\) 個盒子中選 \(k\) 個盒子是非空的,方案數為 \(C_x^k\)。乘上 \(n\) 個不同的球放到 \(k\) 個相同的盒子里的方案數 \(S(n,k)\)。除此之外,因為盒子是區分的,所以還要乘以 \(k!\)。於是可以得到 \(x^n=\sum\limits_{k=0}^n S(n,k) \cdot C_x^k \cdot k!\)

又因為 \(x^{\underline k}=\frac{x!}{(x-k)!}\),於是就可以得到上面那個式子。

其實這個是可以 推出來 的。

補充:有三種說法, \(k\) 的上界分別是 \(n,x,\min(n,x)\)。因為 \(k>x\)\(k>n\) 的時候對答案的貢獻為 \(0\),所以三種都行。

關於下降冪的一個性質:

\(\displaystyle\binom{n}{i} i^{\underline j}=\binom{n-j}{i-j} n^{\underline j}\)

直接展開就行了。

八、圖的計數

以下 \(n\) 均表示節點個數。

01 帶編號無向圖

求:帶編號無向圖的個數。(帶編號就是說,點與點之間是區分的)

圖與圖之間不同,當且僅當它們之間有邊不同。

考慮每一條邊是否選。

因為有 \(n\) 個節點,所以無向邊的數量為 \(\tbinom{n}{2}\)。每條邊可以選或不選,那么就有 \(2^{\tbinom{n}{2}}\) 種方案。

\(n\) 個節點的帶編號無向圖的數量為 \(2^{\tbinom{n}{2}}\)

02 帶編號無向連通圖

\(g(n)\) 表示 \(n\) 個點的無向圖個數(由上可知 \(g(n)=2^{\tbinom n 2}\)),\(f(n)\) 表示 \(n\) 個點的無向連通圖個數。

枚舉普通無向圖中 \(1\) 號點所在連通塊的大小。嘗試用 \(f\) 來表示 \(g\)

具體來說,我們從 \(1\)\(n\) 枚舉 \(1\) 號點所在連通塊的大小 \(i\)。目前只確定了這個連通塊中有 \(1\) 號點, 那么剩下的 \(i-1\) 個點要從剩下的 \(n-1\) 個點中選(從剩下的 \(n-1\) 個點中選與 \(1\) 號點在同一個連通塊的 \(i-1\) 個點),方案數為 \(\dbinom{n-1}{i-1}\)。點集已經確定,\(i\) 個點的連通塊的構成方式有 \(f_i\) 種。除此之外,剩下的點沒有任何限制,所以選擇連通塊以外的點的構成方式有 \(g(n-i)\) 種。則:

\(g(n)=\displaystyle\sum\limits_{i=1}^n \binom{n-1}{i-1} \cdot f(i) \cdot g(n-i)\)

於是我們可以根據這個式子算出 \(f(n)\),即:

\(f(n)=\displaystyle g(n)-\sum\limits_{i=1}^{n-1} \binom{n-1}{i-1} \cdot f(i) \cdot g(n-i)\)

03 帶編號偶度點圖

構造方法是,對於前 \(n−1\) 個點任意連邊,第 \(n\) 個點向所有奇度點連邊(可以證明奇度點一定有偶數個)。

所以答案為 \(2^{\tbinom{n-1}{2}}\)

04 帶編號偶度連通圖

與帶編號無向連通圖的計算方法類似。

\(g(n)\) 表示 \(n\) 個點的偶度點圖個數(由上可知 \(2^{\tbinom{n-1}{2}}\)),\(f(n)\) 表示 \(n\) 個點的偶度點連通圖個數。

枚舉偶度點圖中 \(1\) 號點所在連通塊的大小。

\(\displaystyle g(n)=\sum\limits_{i=1}^n \binom{n-1}{i-1} \cdot f(i) \cdot g(n-i)\)

\(\displaystyle f(n)=g(n)-\sum\limits_{i=1}^{n-1} \binom{n-1}{i-1} \cdot f(i) \cdot g(n-i)\)

九、無根樹 / Prufer 序列

01 無根樹轉 Prufer 序列

每次選擇一個編號最小的葉結點(度數為 \(1\))並刪掉它,然后在序列中記錄下它連接到的那個結點。重復 \(n-2\) 次后就只剩下兩個結點,算法結束。

  • Prufer 序列的長度為 \(n-2\)

  • 每個點在 Prufer 序列中出現的次數為其度數 \(-1\)

02 Prufer 序列轉無根樹

每次選擇一個度數為 \(1\) 的最小的編號節點,與當前枚舉到的 Prufer 序列的點之間連上一條邊,然后同時將兩個點的度數 \(-1\)。最后在剩下的兩個度為 \(1\) 的點間連一條邊。

注意到,點數為 \(n\) 的無根樹與長度為 \(n−2\) 的 Prufer 序列一一對應。

03 完全圖的生成樹數量

注意到實際上要求的就是長度為 \(n-2\),值域為 \(1\sim n\) 的 Prufer 序列的數量,於是方案數即為 \(n^{n-2}\)

04 給定 n 個節點的度數,求無根樹的個數

已知每個節點的度數,也就是說每個元素在 Prufer 序列中出現的次數是確定的(即 \(i\) 在數列中恰好出現 \(d_i-1\) 次),於是問題轉化成為求可重集的排列個數。答案即為:

 \(\displaystyle\frac{(n-2)!}{\prod\limits_{i=1}^n (d_i-1)!}\)

05 計算完全二分圖 K(n,m) 的生成樹個數

注意到生成樹的 Prufer 序列中一定有 \(n-1\) 個左側的點,\(m-1\) 個右側的點,於是答案即為 \(n^{m-1} \cdot m^{n-1}\)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM