[2019 牛客CSP-S提高组赛前集训营4题解] 复读数组(数论)+ 路径计数机(数上DP)+ 排列计数机(线段树+二项式定理)

T1:复读数组

题目

有一个长为n×k的数组,它是由长为n的数组A1,A2,…,An重复k次得到的。
定义这个数组的一个区间的权值为它里面不同的数的个数,
现在,你需要求出对于这个数组的每个非空区间的权值之和。
答案对10^9+7取模
点击下载大样例
输入描述:
第一行两个整数n和k。
接下来一行n个整数,第i个整数为Ai
输出描述:
输出一个整数,表示答案。

示例1
输入
2 2
1 2
输出
16
说明
数组为1, 2, 1, 2
对于长为1的区间,共4个,权值为1
对于长度>1的区间,可以发现权值均为2,共6个
那么权值和为1×4+2×6=16

备注:
对于前10%的数据n≤5
对于前20%的数据n≤100
对于前40%的数据n≤1000
对于另外10%的数据n≤100,k=1
对于另外10%的数据n≤1000,k=1
对于另外10%的数据n≤105,k=1
对于所有数据,1≤n≤105,1≤k≤109,1≤Ai≤109

题解

我们来考虑某一个a[x]如果出现在一个区间 [ l , r ] [l,r] [l,r],那么就会对这个区间贡献1
所以我们可以去找有多少个区间包含a[x],这些区间的贡献就会+1

但是马上我们就意识到一个区间可能多个a[x],则这个区间a[x]的贡献就多算了
于是我们调转思路,去找多少个区间不包含a[x]


先不考虑k的限制,就看 [ l , r ] [l,r] [l,r]
在这里插入图片描述
观察这幅图,假设a[x]在区间出现的位置分别在a,b,c
那么不包含a[x]的区间会在哪些地方取呢??易得如下图:
在这里插入图片描述
红色区域就是不包含a[x]的所有可能区间,
接着我们来计算着一个区间中有多少种 [ l , r ] [l,r] [l,r]
举栗说明:
假设a[x]出现在4,12,那么 [ 5 , 10 ] [5,10] [5,10]就是可选的区间,对于不同的l,对应的r个数也不一样

lr
55,6,7,8,9,10
66,7,8,9,10
77,8,9,10
88,9,10
99,10
1010

有木有发现规律,这其实就是一个等差数列,令T=b-a-1(真正能选区间的端点个数)
则答案个数就是 T ∗ ( T + 1 ) / 2 T*(T+1)/2 T(T+1)/2


接下来我们把k的限制考虑进去,看图↓
在这里插入图片描述
如果 [ 1 , n ] [1,n] [1,n]的黄色区间就对应 [ n + 1 , 2 n ] [n+1,2n] [n+1,2n] [ 2 n + 1 , 3 n ] [2n+1,3n] [2n+1,3n]中的黄色区间
是不是他们是完全相等的, [ 1 , n ] [1,n] [1,n]中黄色区间的答案个数就是后面对应区间的个数
那么一个有k个这样的区间,所以…是不是乘个k

怎么算呢?肯定是上面说的等差数列,那么这个区间端点个数T又有多少个呢? ( r − l − 1 ) (r-l-1) (rl1)
举个栗子:a[x]出现在4,12,则可选端点就是 [ 5 , 11 ] [5,11] [5,11],可取的左右端点就是 l + 1 l+1 l+1 r − 1 r-1 r1
即是 ( r − 1 − ( l + 1 ) + 1 ) = ( r − l − 1 ) (r-1-(l+1)+1)=(r-l-1) (r1(l+1)+1)=(rl1)


如果是交叉了两个块怎么办呢??如图↓
在这里插入图片描述
与上面情况一样处理,但是我们发现这个时候只会有k-1个,所以…懂了吧!

怎么算呢?照样是等差数列,但是端点个数T发生了改变
推理一波:能跨块的区间是不是a[x]在 [ 1 , n ] [1,n] [1,n]区间最后一次出现的位置到a[x]在 [ 1 , n ] [1,n] [1,n]第一次出现的位置
只不过此时a[x]相对应在了第二个区间块

考虑在第一个区间块的可取端点为 [ l + 1 , n ] [l+1,n] [l+1,n],个数就是 n − ( l + 1 ) + 1 = n − l n-(l+1)+1=n-l n(l+1)+1=nl
考虑跨越在了第二个区间块的可取端点为 [ 1 , r − 1 ] [1,r-1] [1,r1],个数就是 r − 1 − 1 + 1 = r − 1 r-1-1+1=r-1 r11+1=r1
把二者加在一起即是所有可取的端点个数了


代码实现

#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;
		else
			a[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:路径计数机

题目

有一棵n个点的树和两个整数p, q,求满足以下条件的四元组(a, b, c, d)的个数:

  1. 1⩽a,b,c,d⩽n。
  2. 点a到点b的经过的边数为p。
  3. 点c到点d的经过的边数为q。
  4. 不存在一个点,它既在点a到点b的路径上,又在点c到点d的路径上。
    点击下载大样例

输入描述:
第一行三个整数n,p,q。
接下来n - 1行,每行两个整数u, v,表示树上存在一个连接点u和点v的边。
输出描述:
输出一个整数,表示答案。
示例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%的数据,n,p,q≤50。
对于前40%的数据,n,p,q≤200。
对于另外10%的数据,p = 2, q = 2。
对于另外10%的数据,树是一条链。
对于另外10%的数据,树随机生成。
对于所有数据1≤n,p,q≤3000,1≤u,v≤n,保证给出的是一棵合法的树。

题解

其实这道题与集训营1的B题思路上很相似,下面就写得比较乱,如果想理解明白一点的,可以移步我之前的博客它会让你耳目一新,里面的讲解很清楚

话不多说,直接上思路,本蒟蒻还有很多题解没打
正难反易,考虑反求问题,不相交的路径数=所有路径数-相交路径数

思考哪些情况,存在一个点,它既在点a到点b的路径上,又在点c到点d的路径上
如果我们固定了a到b的路径,那么这个特殊的点会出现在哪里,肯定会经过 l c a ( a , b ) lca(a,b) lca(a,b)


情况1:c和d均在lca的子树内,两者相连必须经过lca
在这里插入图片描述
情况2:c和d通过lca相连
在这里插入图片描述
情况3:c和d在a到lca或者b到lca路径上相连
在这里插入图片描述
不难发现如果我们把a的父亲,lca的儿子假设成新的lca,上面的情况2和情况3都是一样的,而且c到d的绿色路径总是经过lca的
他可以是两条都在子树内的链或者是一条子树内的链和一条从子树内(可以不进)到子树外的链
这启示我们可以通过枚举lca来进行转移


d p [ u ] [ j ] dp[u][j] dp[u][j]表示:u的子树内,与u的距离为j的节点个数
那么转移就很简单,通过儿子v完成
d p [ u ] [ j ] + = d p [ v ] [ j − 1 ] dp[u][j]+=dp[v][j-1] dp[u][j]+=dp[v][j1]
f p [ u ] fp[u] fp[u]表示:情况1两条都在子树内的链的距离为p的方案数量
那么每个点都会与u的其它子树链上点构成一种方案,所以我们可以规定顺序,x与之前搜索到的点进行匹配,后面的点再与前面的点进行匹配,这样就不会多算
f p [ u ] = d p [ u ] [ p − j ] ∗ d p [ v ] [ j − 1 ] fp[u]=dp[u][p-j]*dp[v][j-1] fp[u]=dp[u][pj]dp[v][j1]
这样保证了,一定会经过u成为lca的这个点
那么情况1中距离为q的方案数量与上面的转移则是一模一样,不再重复

接着设 g p [ v ] [ j ] gp[v][j] gp[v][j]表示:除开v的子树,整棵树与v的距离为j的节点个数,来处理情况2
我们用v的父亲u来更新v,v外面的点距离u的距离应该是j-1,那么会出现这种情况,与集训营1的一道题类似,在v的子树内的点距离v就不会是j-1,而应该是j-2,要减掉
在这里插入图片描述
g [ v ] [ j ] + = g [ u ] [ j − 1 ] + d p [ u ] [ j − 1 ] − d p [ v ] [ j − 2 ] g[v][j] += g[u][j - 1] + dp[u][j - 1] - dp[v][j - 2] g[v][j]+=g[u][j1]+dp[u][j1]dp[v][j2]
接着就是与上面一样的思路,边DP便进行更新
g p [ u ] + = d p [ u ] [ p − i ] ∗ g [ u ] [ i ] gp[u] += dp[u][p - i] * g[u][i] gp[u]+=dp[u][pi]g[u][i]
g q [ u ] + = d p [ u ] [ q − i ] ∗ g [ u ] [ i ] gq[u] += dp[u][q - i] * g[u][i] gq[u]+=dp[u][qi]g[u][i]
最后算出来的方案数要 ∗ 4 *4 4,因为点对顺序不一样算不同的方案,看样例就可知了

代码实现

#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:排列计数机

题目

定义一个长为k的序列 A 1 , A 2 , … , A k A_1, A_2, \dots, A_k A1,A2,,Ak的权值为:对于所有 1 ≤ i ≤ k , max ⁡ ( A 1 , A 2 , … , A i ) 1 \le i \le k,\max(A_1, A_2, \dots, A_i) 1ikmax(A1,A2,,Ai)有多少种不同的取值。
给出一个1到n的排列 B 1 , B 2 , … , B n B_1, B_2, \dots, B_n B1,B2,,Bn,求B的所有非空子序列的权值的m次方之和。
答案对 1 0 9 + 7 10^9 + 7 109+7取模。

点击下载大样例
输入描述:
第一行两个整数n、m。
接下来一行n个整数,第i个整数为 B i B_i Bi
输出描述:
输出一个整数,表示答案。

示例1
输入
3 2
1 3 2
输出
16
说明
在所有非空子序列中:
(1), (3), (2), (3, 2)权值为1,
(1, 3), (1, 2), (1, 3, 2)权值为2。
那么所有非空子序列权值的2次方和为 4 × 1 2 + 3 × 2 2 = 16 4 \times 1^2 + 3 \times 2^2 = 16 4×12+3×22=16
备注:
对于前 10 % 10\% 10%的数据, n ≤ 20 n \le 20 n20
对于前 20 % 20\% 20%的数据, n ≤ 100 n \le 100 n100
对于前 40 % 40\% 40%的数据, n ≤ 1000 n \le 1000 n1000
对于另外 20 % 20\% 20%的数据,m = 1。
对于所有数据, 1 ≤ n ≤ 1 0 5 1 \le n \le 10^5 1n105 1 ≤ m ≤ 20 1 \le m \le 20 1m20,保证B是1到n的排列。

题解

考虑从左到右一个一个加入数
当加入一个数的时候,只有最大值小于这个数的子序列,权值才会被更新( + 1 +1 +1
但我们不可能把每个子序列的权值都求出来后才乘方再加起来,三秒也会T得你怀疑人生
在这里插入图片描述
但我们根据二项式定理,可以发现对于任意一个数x
( x + 1 ) m = C m 0 x m + C m 1 x m − 1 + C m 2 x m − 2 + ⋯ ⋯ + C m m − 2 x 2 + C m m − 1 x 1 + C m m x 0 (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=Cm0xm+Cm1xm1+Cm2xm2++Cmm2x2+Cmm1x1+Cmmx0
通过这个,启发我们可以维护每个子序列权值的m次方和,m-1次方和,m-2次方和…1次方和,0次方和,就可以通过上面的公式得到这些子序列权值集体加1后的乘方的和

由于每次插入值的时候更新只跟最大值有关,而且题目中保证了B是一个排列,因此分情况处理:
1.我们可以把最大值相同的子序列一起处理,维护它们的m次方和,m-1次方和,m-2次方和
加入一个新的数的时候,找到所有最大值比它小的子序列,将它们的m次方和,m-1次方和…加起来,再用二项式定理得到加1后的m次方和,得到一组新的子序列的信息

2.对于那些最大值比它大的子序列,因为无法更新但可以形成新的子序列,使得答案为x的子序列多了整整一倍,所以直接将和 ∗ 2 *2 2就好了

这个可以用线段树维护,线段树的每个位置维护相同最大值的子序列的一些信息

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]维护的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 );
    else
        add ( 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;
}

在这里插入图片描述终于补了这道题,这篇blog也算是有头的了,就bb咯!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值