链接:https://www.nowcoder.com/acm/contest/147/H
来源:牛客网
时间限制:C/C++ 3秒,其他语言6秒
空间限制:C/C++ 262144K,其他语言524288K
64bit IO Format: %lld
题目描述
Niuniu has learned prefix sum and he found an interesting about prefix sum.
Let's consider (k+1) arrays a[i] (0 <= i <= k)
The index of a[i] starts from 1.
a[i] is always the prefix sum of a[i-1].
"always" means a[i] will change when a[i-1] changes.
"prefix sum" means a[i][1] = a[i-1][1] and a[i][j] = a[i][j-1] + a[i-1][j] (j >= 2)
Initially, all elements in a[0] are 0.
There are two kinds of operations, which are modify and query.
For a modify operation, two integers x, y are given, and it means a[0][x] += y.
For a query operation, one integer x is given, and it means querying a[k][x].
As the result might be very large, you should output the result mod 1000000007.
输入描述:
The first line contains three integers, n, m, k.
n is the length of each array.
m is the number of operations.
k is the number of prefix sum.
In the following m lines, each line contains an operation.
If the first number is 0, then this is a change operation.
There will be two integers x, y after 0, which means a[0][x] += y;
If the first number is 1, then this is a query operation.
There will be one integer x after 1, which means querying a[k][x].
1 <= n <= 100000
1 <= m <= 100000
1 <= k <= 40
1 <= x <= n
0 <= y < 1000000007
输出描述:
For each query, you should output an integer, which is the result.
输入
4 11 3
0 1 1
0 3 1
1 1
1 2
1 3
1 4
0 3 1
1 1
1 2
1 3
1 4
输出
1
3
7
13
1
3
8
16
说明
For the first 4 queries, the (k+1) arrays are
1 0 1 0
1 1 2 2
1 2 4 6
1 3 7 13
For the last 4 queries, the (k+1) arrays are
1 0 2 0
1 1 3 3
1 2 5 8
1 3 8 16
题意:一个矩阵A有n列,有两种操作:
输入0 x y 表示矩阵A[0][x]位置增加y值
输入1 y 表示查询矩阵A[k][y]的值
现在有m个这样的操作,输入k表示只查询第k行的各值
该矩阵存在以下递推式:a[i][1] = a[i-1][1] 【当为第1列时,直接传递该列正上方的值】
a[i][j] = a[i][j-1] + a[i-1][j] (j >= 2) 【第i行第j列的值等于其上方和左方位置两值之和】
题解:首先很明显这样的矩阵递推方式相当于第i行即i-1行的前缀和,那么第k行即第0行更新之后的第K个前缀和。
关于递推的内容很容易想到最简单的递推——杨辉三角,同时这也是组合数表能够直接计算出来的值。如下所示:
...........组合数
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1
..........递推值
预处理组合数以O(1)计算出来的各值就是不带前缀和的情况下通过每个值的左上角和正上方两数求和得到的。
规范来说即C[i][j]=C[i-1][j-1]+C[i-1][j]
但我们要求的递推形式不是这样的,而是左方和上方两值之和,形如下方的递推形式
1 1 1 1
1 2 3 4
1 3 6 10
1 4 10 20
............前缀和
如果通过组合数的计算方式得到这样递推式,从组合数值表中我们竖向或者斜向观察,可以看出,刚好符合该前缀和式的递推计算方法。
由此通过组合数计算得到如下规律:
当第我们对矩阵A【0】【x】进行修改时,对矩阵A【k】【y】的影响是 【斜向】
或者是 【竖向】
有两种形式符合这样的递推方式是因为组合数有对称的形式,如 与 算出的结果是一样的。
得到了每次修改对结果的贡献后,因为k很小因此可以直接预处理出40以内的所有组合数。直接对每个更新操作计算其对查询操作所造成的影响,但是,每个操作都可能对其后几乎每一个位置的查询造成影响。如果m次操作安排的是一半更新一半查询,且查询位置都在更新之后。这样的复杂度是O(nm)的。
题解中有这样的内容:
如何理解这样的暴力呢?
首先是第一种O(1)更改,O(n)查询,很明显就是上面提到的组合数计算,只对第0行进行O(1)的修改,但是因为其影响范围广,所以一个查询就要计算大量的组合数并求和。也就是O(n)的
那么第二种暴力,O(n)的修改和O(1)的查询,也就是说,对于每个修改,我们不止对第0行进行一次修改,还要暴力的将该更新影响到的位置全部传递下去,这么做就是遍历了O(n*k)的矩阵,而查询,就是直接输出第k行y列的值即可,无须任何组合数或求和的操作,即查询O(1)。
将两种暴力巧妙的结合一下,即在不同的数据量用不同的方式更新和查询,即能找到两种暴力复杂度的一个平衡点。如我们知道了第一种暴力,其修改很快,但是对于大量的修改,查询就要进行大量的求和。那么我们尽量在一个较小的处理区间内使用第一种暴力。
而第二种暴力,大型的区间我们打包集体用n*k的复杂度计算,其贡献能够以最小的O(1)查询,但是一旦处理的区间较小时,可以发现,对于一次修改,其影响就是n*k,那么m次修改,要用这样暴力的方法更新就是n*m*k的复杂度,最裸的暴力,一定不可取。
因此我们将一个较大的处理区间全部更新到第0行上。然后将其一起做一个遍历整个矩阵的n*k的更新。随着处理的更新数量越来越少,我们就用第一种暴力直接计算即可。
关于实现,这样分种类的处理方法可以用分块暴力,也能用CDQ分治,无论分块还是分治,其思想都是大问题用一个方法处理,分解成小问题后再用另一种合适的方法处理。
如分块其实就是,我们累积连续的更新操作,当连续的更新数量超过某个临界值,如2000时,我们将这些操作打包用第二种暴力处理。而不足两千更新就查询的区间,我们用第一种暴力直接计算即可。取两种暴力的优势,避免其劣势。最大可能的减小复杂度。
而CDQ分治这种优雅的做法,考虑了分治处理时,L区间对于R区间存在一定影响,因此优先处理其造成的影响。而在正在处理区间内的查询,在更小的区间中再计算其受到的影响。
如上图,很不清晰的展示了,若正在处理L区间,那么L区间内的更新将全部打包传递,但查询是被忽略的,L区间内的查询要等待处理到小区间时才有机会被解决。而且必定是在R区间内被解决。
有一点要注意到是,组合数要计算到2e5的值,否则会WA
分析下整体复杂度:len>2000的递归次数最坏情况下(n=m=100000)约为15次,所以这部分复杂度为O(15n), len<2000的递归次数约为m次,每次复杂度均摊log²(m)次,所以这部分复杂度O(mlogm),整体复杂度O(15n+mlog²m)
最后,代码如下:
#include<bits/stdc++.h>
#define LL long long
#define M(a,b) memset(a,b,sizeof a)
#define pb(x) push_back(x)
using namespace std;
const int maxn=2e5+7;
const int mod=1000000007;
int n,m,k;
struct node
{
int x,t,op;///op表示操作标识,查询或是更新,t表示操作顺序标号,x表示操作位置,即列数
LL y;///x 表示列,y表示增加的值
} pro[maxn],update[maxn],query[maxn];///pro记录操作,update分类更新操作,query分类查询操作
LL ans[maxn],mp[45][maxn],C[maxn][45];///ans记录每个查询操作的结果,按序输出, mp表示实际矩阵,C表示预处理组合数
void init()///预处理组合数
{
for(int i=0; i<=maxn; i++)C[i][0]=1;
for(int i=1; i<=maxn; i++)
for(int j=1; j<=40; j++)
C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
}
void CDQ(int l,int r)
{
int mid=(l+r)>>1;///二分操作
if(l>=r)return;
if(mid-l+1>=1000)///CDQ得到的操作次数是大于2000的区间
{
for(int i=0; i<=n; i++)mp[0][i]=0;///初始化
for(int i=l; i<=mid; i++) if(!pro[i].op) mp[0][pro[i].x]=(mp[0][pro[i].x]+pro[i].y)%mod;///左区间更新(只更新第一行)
for(int i=1; i<=k+1; i++)///直接从第一行传递到第k+1行(暴力传递,相当于也计算了前缀和)
for(int j=1; j<=n; j++)
mp[i][j]=(mp[i-1][j]+mp[i][j-1])%mod;///直接加上
for(int i=mid+1; i<=r; i++) if(pro[i].op) ans[pro[i].t] = (ans[pro[i].t]+mp[k+1][pro[i].x])%mod;
}///右区间查询答案赋值,由之前左边的影响直接传递到实际矩阵中,并更新ans
else
{
int upt=0,qpt=0;///将操作分类
for(int i=l; i<=mid; i++)if(!pro[i].op)update[upt++]=pro[i];///找到左区间的更新项
for(int i=mid+1; i<=r; i++)if(pro[i].op)query[qpt++]=pro[i];///右区间的查询项
for(int i=0; i<upt; i++)
for(int j=0; j<qpt; j++)
if(update[i].x<=query[j].x) ans[query[j].t] = (ans[query[j].t]+update[i].y*C[k+query[j].x-update[i].x][k]%mod)%mod;
}
CDQ(l,mid);///先处理左边再处理右边,这样右边会得到左边的影响
CDQ(mid+1,r);
}
int main()
{
M(ans,-1);
init();
scanf("%d%d%d",&n,&m,&k);///n列,m次操作,查询第k行
k--;
for(int i=1; i<=m; i++)
{
scanf("%d%d",&pro[i].op,&pro[i].x);
pro[i].t=i;
if(pro[i].op) ans[i]=0;
else scanf("%lld",&pro[i].y);
}
CDQ(1,m);
for(int i=1; i<=m; i++)if(ans[i]!=-1)printf("%lld\n",ans[i]);
}