[2019 牛客CSP-S提高组赛前集训营4题解] 复读数组(数论)+ 路径计数机(数上DP)+ 排列计数机(线段树+二项式定理)
文章目錄
- T1:復(fù)讀數(shù)組
- 題目
- 題解
- 代碼實現(xiàn)
- T2:路徑計數(shù)機(jī)
- 題目
- 題解
- 代碼實現(xiàn)
- T3:排列計數(shù)機(jī)
- 題目
- 題解
- CODE
T1:復(fù)讀數(shù)組
題目
有一個長為n×k的數(shù)組,它是由長為n的數(shù)組A1,A2,…,An重復(fù)k次得到的。
定義這個數(shù)組的一個區(qū)間的權(quán)值為它里面不同的數(shù)的個數(shù),
現(xiàn)在,你需要求出對于這個數(shù)組的每個非空區(qū)間的權(quán)值之和。
答案對10^9+7取模
點擊下載大樣例
輸入描述:
第一行兩個整數(shù)n和k。
接下來一行n個整數(shù),第i個整數(shù)為Ai
輸出描述:
輸出一個整數(shù),表示答案。
示例1
輸入
2 2
1 2
輸出
16
說明
數(shù)組為1, 2, 1, 2
對于長為1的區(qū)間,共4個,權(quán)值為1
對于長度>1的區(qū)間,可以發(fā)現(xiàn)權(quán)值均為2,共6個
那么權(quán)值和為1×4+2×6=16
備注:
對于前10%的數(shù)據(jù)n≤5
對于前20%的數(shù)據(jù)n≤100
對于前40%的數(shù)據(jù)n≤1000
對于另外10%的數(shù)據(jù)n≤100,k=1
對于另外10%的數(shù)據(jù)n≤1000,k=1
對于另外10%的數(shù)據(jù)n≤105,k=1
對于所有數(shù)據(jù),1≤n≤105,1≤k≤109,1≤Ai≤109
題解
我們來考慮某一個a[x]如果出現(xiàn)在一個區(qū)間[l,r][l,r][l,r],那么就會對這個區(qū)間貢獻(xiàn)1
所以我們可以去找有多少個區(qū)間包含a[x],這些區(qū)間的貢獻(xiàn)就會+1
但是馬上我們就意識到一個區(qū)間可能多個a[x],則這個區(qū)間a[x]的貢獻(xiàn)就多算了
于是我們調(diào)轉(zhuǎn)思路,去找多少個區(qū)間不包含a[x]
先不考慮k的限制,就看[l,r][l,r][l,r]
觀察這幅圖,假設(shè)a[x]在區(qū)間出現(xiàn)的位置分別在a,b,c
那么不包含a[x]的區(qū)間會在哪些地方取呢??易得如下圖:
紅色區(qū)域就是不包含a[x]的所有可能區(qū)間,
接著我們來計算著一個區(qū)間中有多少種[l,r][l,r][l,r]
舉栗說明:
假設(shè)a[x]出現(xiàn)在4,12,那么[5,10][5,10][5,10]就是可選的區(qū)間,對于不同的l,對應(yīng)的r個數(shù)也不一樣
| 5 | 5,6,7,8,9,10 |
| 6 | 6,7,8,9,10 |
| 7 | 7,8,9,10 |
| 8 | 8,9,10 |
| 9 | 9,10 |
| 10 | 10 |
有木有發(fā)現(xiàn)規(guī)律,這其實就是一個等差數(shù)列,令T=b-a-1(真正能選區(qū)間的端點個數(shù))
則答案個數(shù)就是T?(T+1)/2T*(T+1)/2T?(T+1)/2
接下來我們把k的限制考慮進(jìn)去,看圖↓
如果[1,n][1,n][1,n]的黃色區(qū)間就對應(yīng)[n+1,2n][n+1,2n][n+1,2n],[2n+1,3n][2n+1,3n][2n+1,3n]中的黃色區(qū)間
是不是他們是完全相等的,[1,n][1,n][1,n]中黃色區(qū)間的答案個數(shù)就是后面對應(yīng)區(qū)間的個數(shù)
那么一個有k個這樣的區(qū)間,所以…是不是乘個k
怎么算呢?肯定是上面說的等差數(shù)列,那么這個區(qū)間端點個數(shù)T又有多少個呢?(r?l?1)(r-l-1)(r?l?1),
舉個栗子:a[x]出現(xiàn)在4,12,則可選端點就是[5,11][5,11][5,11],可取的左右端點就是l+1l+1l+1和r?1r-1r?1
即是(r?1?(l+1)+1)=(r?l?1)(r-1-(l+1)+1)=(r-l-1)(r?1?(l+1)+1)=(r?l?1)
如果是交叉了兩個塊怎么辦呢??如圖↓
與上面情況一樣處理,但是我們發(fā)現(xiàn)這個時候只會有k-1個,所以…懂了吧!
怎么算呢?照樣是等差數(shù)列,但是端點個數(shù)T發(fā)生了改變
推理一波:能跨塊的區(qū)間是不是a[x]在[1,n][1,n][1,n]區(qū)間最后一次出現(xiàn)的位置到a[x]在[1,n][1,n][1,n]第一次出現(xiàn)的位置
只不過此時a[x]相對應(yīng)在了第二個區(qū)間塊
考慮在第一個區(qū)間塊的可取端點為[l+1,n][l+1,n][l+1,n],個數(shù)就是n?(l+1)+1=n?ln-(l+1)+1=n-ln?(l+1)+1=n?l
考慮跨越在了第二個區(qū)間塊的可取端點為[1,r?1][1,r-1][1,r?1],個數(shù)就是r?1?1+1=r?1r-1-1+1=r-1r?1?1+1=r?1
把二者加在一起即是所有可取的端點個數(shù)了
代碼實現(xiàn)
#include <cstdio> #include <vector> #include <algorithm> using namespace std; #define mod 1000000007 #define LL long long #define MAXN 100005 struct node {int id, val; }b[MAXN]; vector < int > G[MAXN]; int n; int a[MAXN]; LL result, k, tot;bool cmp ( node x, node y ) {return x.val < y.val; }LL count ( LL x ) {return ( ( x * ( x + 1 ) ) >> 1 ) % mod; }int main() {scanf ( "%d %lld", &n, &k );for ( int i = 1;i <= n;i ++ ) {scanf ( "%d", &a[i] );b[i].val = a[i];b[i].id = i;}sort ( b + 1, b + n + 1, cmp );for ( int i = 1;i <= n;i ++ )if ( b[i].val != b[i - 1].val )a[b[i].id] = ++ tot;elsea[b[i].id] = tot;for ( int i = 1;i <= n;i ++ )G[a[i]].push_back( i );result = tot * count ( k * n % mod ) % mod;for ( int i = 1;i <= tot;i ++ ) {for ( int j = 1;j < G[i].size();j ++ )result = ( result - k * count ( G[i][j] - G[i][j - 1] - 1 ) % mod + mod ) % mod;result = ( result - ( k - 1 ) * count ( n - G[i][G[i].size() - 1] + G[i][0] - 1 ) % mod + mod ) % mod;result = ( result - count ( G[i][0] - 1 ) + mod ) % mod;result = ( result - count ( n - G[i][G[i].size() - 1] ) + mod ) % mod;}printf ( "%lld", result % mod );return 0; }T2:路徑計數(shù)機(jī)
題目
有一棵n個點的樹和兩個整數(shù)p, q,求滿足以下條件的四元組(a, b, c, d)的個數(shù):
點擊下載大樣例
輸入描述:
第一行三個整數(shù)n,p,q。
接下來n - 1行,每行兩個整數(shù)u, v,表示樹上存在一個連接點u和點v的邊。
輸出描述:
輸出一個整數(shù),表示答案。
示例1
輸入
5 2 1
1 2
2 3
3 4
2 5
輸出
4
說明
合法的四元組一共有:
(1, 5, 3, 4),
(1, 5, 4, 3),
(5, 1, 3 ,4),
(5, 1, 4, 3)。
示例2
輸入
4 1 1
1 2
2 3
3 4
輸出
8
備注:
對于前20%的數(shù)據(jù),n,p,q≤50。
對于前40%的數(shù)據(jù),n,p,q≤200。
對于另外10%的數(shù)據(jù),p = 2, q = 2。
對于另外10%的數(shù)據(jù),樹是一條鏈。
對于另外10%的數(shù)據(jù),樹隨機(jī)生成。
對于所有數(shù)據(jù)1≤n,p,q≤3000,1≤u,v≤n,保證給出的是一棵合法的樹。
題解
其實這道題與集訓(xùn)營1的B題思路上很相似,下面就寫得比較亂,如果想理解明白一點的,可以移步我之前的博客它會讓你耳目一新,里面的講解很清楚
話不多說,直接上思路,本蒟蒻還有很多題解沒打
正難反易,考慮反求問題,不相交的路徑數(shù)=所有路徑數(shù)-相交路徑數(shù)
思考哪些情況,存在一個點,它既在點a到點b的路徑上,又在點c到點d的路徑上
如果我們固定了a到b的路徑,那么這個特殊的點會出現(xiàn)在哪里,肯定會經(jīng)過lca(a,b)lca(a,b)lca(a,b)
情況1:c和d均在lca的子樹內(nèi),兩者相連必須經(jīng)過lca
情況2:c和d通過lca相連
情況3:c和d在a到lca或者b到lca路徑上相連
不難發(fā)現(xiàn)如果我們把a(bǔ)的父親,lca的兒子假設(shè)成新的lca,上面的情況2和情況3都是一樣的,而且c到d的綠色路徑總是經(jīng)過lca的
他可以是兩條都在子樹內(nèi)的鏈或者是一條子樹內(nèi)的鏈和一條從子樹內(nèi)(可以不進(jìn))到子樹外的鏈
這啟示我們可以通過枚舉lca來進(jìn)行轉(zhuǎn)移
設(shè)dp[u][j]dp[u][j]dp[u][j]表示:u的子樹內(nèi),與u的距離為j的節(jié)點個數(shù)
那么轉(zhuǎn)移就很簡單,通過兒子v完成
dp[u][j]+=dp[v][j?1]dp[u][j]+=dp[v][j-1]dp[u][j]+=dp[v][j?1]
設(shè)fp[u]fp[u]fp[u]表示:情況1兩條都在子樹內(nèi)的鏈的距離為p的方案數(shù)量
那么每個點都會與u的其它子樹鏈上點構(gòu)成一種方案,所以我們可以規(guī)定順序,x與之前搜索到的點進(jìn)行匹配,后面的點再與前面的點進(jìn)行匹配,這樣就不會多算
fp[u]=dp[u][p?j]?dp[v][j?1]fp[u]=dp[u][p-j]*dp[v][j-1]fp[u]=dp[u][p?j]?dp[v][j?1]
這樣保證了,一定會經(jīng)過u成為lca的這個點
那么情況1中距離為q的方案數(shù)量與上面的轉(zhuǎn)移則是一模一樣,不再重復(fù)
接著設(shè)gp[v][j]gp[v][j]gp[v][j]表示:除開v的子樹,整棵樹與v的距離為j的節(jié)點個數(shù),來處理情況2
我們用v的父親u來更新v,v外面的點距離u的距離應(yīng)該是j-1,那么會出現(xiàn)這種情況,與集訓(xùn)營1的一道題類似,在v的子樹內(nèi)的點距離v就不會是j-1,而應(yīng)該是j-2,要減掉
g[v][j]+=g[u][j?1]+dp[u][j?1]?dp[v][j?2]g[v][j] += g[u][j - 1] + dp[u][j - 1] - dp[v][j - 2]g[v][j]+=g[u][j?1]+dp[u][j?1]?dp[v][j?2]
接著就是與上面一樣的思路,邊DP便進(jìn)行更新
gp[u]+=dp[u][p?i]?g[u][i]gp[u] += dp[u][p - i] * g[u][i]gp[u]+=dp[u][p?i]?g[u][i]
gq[u]+=dp[u][q?i]?g[u][i]gq[u] += dp[u][q - i] * g[u][i]gq[u]+=dp[u][q?i]?g[u][i]
最后算出來的方案數(shù)要?4*4?4,因為點對順序不一樣算不同的方案,看樣例就可知了
代碼實現(xiàn)
#include <cstdio> #include <vector> #include <iostream> using namespace std; #define MAXN 3005 #define LL long long vector < int > G[MAXN]; int n, p, q; LL result; LL dp[MAXN][MAXN], fp[MAXN], fq[MAXN], g[MAXN][MAXN], gp[MAXN], gq[MAXN];void dfs1 ( int u, int fa ) {dp[u][0] = 1;for ( int i = 0;i < G[u].size();i ++ ) {int v = G[u][i];if ( v == fa )continue;dfs1 ( v, u );for ( int j = 1;j <= p;j ++ )fp[u] += dp[u][p - j] * dp[v][j - 1];for ( int j = 1;j <= q;j ++ )fq[u] += dp[u][q - j] * dp[v][j - 1];for ( int j = 1;j <= p;j ++ )dp[u][j] += dp[v][j - 1];} }void dfs2 ( int u, int fa ) {for ( int i = 1;i <= p;i ++ )gp[u] += dp[u][p - i] * g[u][i];for ( int i = 1;i <= q;i ++ )gq[u] += dp[u][q - i] * g[u][i];for ( int i = 0;i < G[u].size();i ++ ) {int v = G[u][i];if ( v == fa )continue;g[v][1] = 1;for ( int j = 2;j <= p;j ++ )g[v][j] += g[u][j - 1] + dp[u][j - 1] - dp[v][j - 2];dfs2 ( v, u );} }int main() {scanf ( "%d %d %d", &n, &p, &q );if ( p < q )swap ( p, q );for ( int i = 1;i < n;i ++ ) {int u, v;scanf ( "%d %d", &u, &v );G[u].push_back( v );G[v].push_back( u );}dfs1 ( 1, 0 );dfs2 ( 1, 0 );LL sum1 = 0, sum2 = 0;for ( int i = 1;i <= n;i ++ )sum1 += fp[i], sum2 += fq[i];result = sum1 * sum2;for ( int i = 1;i <= n;i ++ )result -= fp[i] * fq[i] + fp[i] * gq[i] + fq[i] * gp[i];printf ( "%lld\n", result * 4 );return 0; }T3:排列計數(shù)機(jī)
題目
定義一個長為k的序列A1,A2,…,AkA_1, A_2, \dots, A_kA1?,A2?,…,Ak?的權(quán)值為:對于所有1≤i≤k,max?(A1,A2,…,Ai)1 \le i \le k,\max(A_1, A_2, \dots, A_i)1≤i≤k,max(A1?,A2?,…,Ai?)有多少種不同的取值。
給出一個1到n的排列B1,B2,…,BnB_1, B_2, \dots, B_nB1?,B2?,…,Bn?,求B的所有非空子序列的權(quán)值的m次方之和。
答案對109+710^9 + 7109+7取模。
點擊下載大樣例
輸入描述:
第一行兩個整數(shù)n、m。
接下來一行n個整數(shù),第i個整數(shù)為BiB_iBi?
輸出描述:
輸出一個整數(shù),表示答案。
示例1
輸入
3 2
1 3 2
輸出
16
說明
在所有非空子序列中:
(1), (3), (2), (3, 2)權(quán)值為1,
(1, 3), (1, 2), (1, 3, 2)權(quán)值為2。
那么所有非空子序列權(quán)值的2次方和為4×12+3×22=164 \times 1^2 + 3 \times 2^2 = 164×12+3×22=16
備注:
對于前10%10\%10%的數(shù)據(jù),n≤20n \le 20n≤20
對于前20%20\%20%的數(shù)據(jù),n≤100n \le 100n≤100
對于前40%40\%40%的數(shù)據(jù),n≤1000n \le 1000n≤1000
對于另外20%20\%20%的數(shù)據(jù),m = 1。
對于所有數(shù)據(jù),1≤n≤1051 \le n \le 10^51≤n≤105,1≤m≤201 \le m \le 201≤m≤20,保證B是1到n的排列。
題解
考慮從左到右一個一個加入數(shù)
當(dāng)加入一個數(shù)的時候,只有最大值小于這個數(shù)的子序列,權(quán)值才會被更新(+1+1+1)
但我們不可能把每個子序列的權(quán)值都求出來后才乘方再加起來,三秒也會T得你懷疑人生
但我們根據(jù)二項式定理,可以發(fā)現(xiàn)對于任意一個數(shù)x
(x+1)m=Cm0xm+Cm1xm?1+Cm2xm?2+??+Cmm?2x2+Cmm?1x1+Cmmx0(x+1)^m=C^0_mx^m+C^1_mx^{m?1}+C^2_mx^{m?2}+??+C^{m?2}_mx^2+C^{m?1}_mx^1+C^m_mx^0(x+1)m=Cm0?xm+Cm1?xm?1+Cm2?xm?2+??+Cmm?2?x2+Cmm?1?x1+Cmm?x0
通過這個,啟發(fā)我們可以維護(hù)每個子序列權(quán)值的m次方和,m-1次方和,m-2次方和…1次方和,0次方和,就可以通過上面的公式得到這些子序列權(quán)值集體加1后的乘方的和
由于每次插入值的時候更新只跟最大值有關(guān),而且題目中保證了B是一個排列,因此分情況處理:
1.我們可以把最大值相同的子序列一起處理,維護(hù)它們的m次方和,m-1次方和,m-2次方和
加入一個新的數(shù)的時候,找到所有最大值比它小的子序列,將它們的m次方和,m-1次方和…加起來,再用二項式定理得到加1后的m次方和,得到一組新的子序列的信息
2.對于那些最大值比它大的子序列,因為無法更新但可以形成新的子序列,使得答案為x的子序列多了整整一倍,所以直接將和?2*2?2就好了
這個可以用線段樹維護(hù),線段樹的每個位置維護(hù)相同最大值的子序列的一些信息
CODE
#include <cstdio> #define mod 1000000007 #define MAXN 100005 int n, m; int a[MAXN], tmp[25], pre[25], sum[MAXN << 2][25], lazy[MAXN << 2][25]; //sum[i][k]維護(hù)的C(k,m)i^m和 int C[25][25];void pushdown ( int t, int l, int r, int k ) {lazy[t << 1][k] = 1ll * lazy[t << 1][k] * lazy[t][k] % mod;lazy[t << 1 | 1][k] = 1ll * lazy[t << 1 | 1][k] * lazy[t][k] % mod;sum[t << 1][k] = 1ll * sum[t << 1][k] * lazy[t][k] % mod;sum[t << 1 | 1][k] = 1ll * sum[t << 1 | 1][k] * lazy[t][k] % mod;lazy[t][k] = 1;return; }void add ( int t, int l, int r, int id, int v, int k ) {if ( l == r ) {sum[t][k] = v;return;}int mid = ( l + r ) >> 1;pushdown ( t, l, r, k );if ( id <= mid )add ( t << 1, l, mid, id, v, k );elseadd ( t << 1 | 1, mid + 1, r, id, v, k );sum[t][k] = ( sum[t << 1][k] + sum[t << 1 | 1][k] ) % mod; }int query ( int t, int l, int r, int id, int k ) {if ( r <= id )return sum[t][k];int mid = ( l + r ) >> 1;pushdown ( t, l, r, k );int sum1 = 0, sum2 = 0;sum1 = query ( t << 1, l, mid, id, k );if ( mid < id )sum2 = query ( t << 1 | 1, mid + 1, r, id, k );return ( sum1 + sum2 ) % mod; }void mul ( int t, int l, int r, int id, int k ) {if ( r < id )return;if ( id <= l ) {sum[t][k] = 2ll * sum[t][k] % mod;lazy[t][k] = lazy[t][k] * 2ll % mod;return;}int mid = ( l + r ) >> 1;pushdown ( t, l, r, k );if ( id <= mid )mul ( t << 1, l, mid, id, k );mul ( t << 1 | 1, mid + 1, r, id, k );sum[t][k] = ( 1ll * sum[t << 1][k] + sum[t << 1 | 1][k] ) % mod; }int main() {C[0][0] = 1;//先打出二項式定理C的表,仗著m小使勁搞for ( int i = 1;i <= 20;i ++ ) {C[i][0] = 1;for ( int j = 1;j <= i;j ++ )C[i][j] = C[i - 1][j - 1] + C[i - 1][j];}scanf ( "%d %d", &n, &m );for ( int i = 1;i <= ( n << 2 );i ++ )for ( int j = 0;j <= m;j ++ )lazy[i][j] = 1;//線段樹初始化,因為我們是乘法所以初始化為1 for ( int i = 1;i <= n;i ++ ) {scanf ( "%d", &a[i] );for ( int j = 0;j <= m;j ++)pre[j] = tmp[j] = 0;for ( int j = 0;j <= m;j ++ )pre[j] = query ( 1, 1, n, a[i], j );for ( int j = m;j >= 0;j -- )for ( int k = j;k >= 0;k -- )tmp[j] = ( tmp[j] + 1ll * pre[k] * C[j][k] % mod ) % mod;for ( int j = 0;j <= m;j ++ ) {add ( 1, 1, n, a[i], tmp[j] + 1, j );mul ( 1, 1, n, a[i] + 1, j );}}printf ( "%d", ( query ( 1, 1, n, n, m ) % mod + mod ) % mod );return 0; }終于補(bǔ)了這道題,這篇blog也算是有頭的了,就bb咯!
總結(jié)
以上是生活随笔為你收集整理的[2019 牛客CSP-S提高组赛前集训营4题解] 复读数组(数论)+ 路径计数机(数上DP)+ 排列计数机(线段树+二项式定理)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 联通怎么设置wifi密码联通路由器怎么设
- 下一篇: [多校联考-西南大学附中]切面包(线段树