牛客周赛113

(0条未读通知) 牛客竞赛_ACM/NOI/CSP/CCPC/ICPC算法编程高难度练习赛_牛客竞赛OJ

E题

首先我们预处理每个数组从\(n\)个数中选择\(i\)个数,其和模495为\(j\)的方案数,可以使用三维\(dp[i][j][k]\)数组表示前\(i\)个数中选\(j\)个数,他们的和对495取模为\(k\)的方案数,初始化\(dp[0][0][0]=1\),可以写出转移方程:

\[dp[i][j][k]=\begin{cases}dp[i-1][j][k]&j=0,\\dp[i-1][j][k]+dp[i-1][j-1][(k-a[i]+495)\%495]&else.\end{cases} \]

这里我们可以使用滚动数组优化掉第一维,用\(dp[i][j]\)表示从\(n\)个数中选\(i\)个数,其和模495为\(j\)的方案数。

vector<vector<int>> dp(n+1,vector<int>(m,0));
	dp[0][0]=1;
	for(int i=1;i<=n;i++){
		auto ndp=dp;
		for(int j=1;j<=n;j++){
			for(int k=0;k<m;k++){
				int v=(a[i]+k)%m;
				ndp[j][v]=(ndp[j][v]+dp[j-1][k])%mod;
			}
		}
		dp=ndp;
	}

之后我们将\(dpa\)\(dpb\)都求出来之和,因为后面求解答案时,我们要枚举\(a\)选了\(i\)个元素,那么\(b\)的贡献为\(\sum_{j=0}^{i}{dpb[j][k]}\),所以我们再对\(b\)求一个前缀和,\(dpb[i][j]\)表示至多选\(i\)个元素时,他们的和模495为\(j\)的方案数之和。

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

这样我们就可以求解答案了,枚举从\(a\)中选择的个数\(i\),枚举\(a\)模495为\(j\),枚举\(b\)模495为\(k\),那么答案为:

vector<int> ans(m,0);
	for(int i=0;i<=n;i++){
		for(int j=0;j<m;j++){
			if(dpa[i][j]==0) continue;
			for(int k=0;k<m;k++){
				if(dpb[i][k]==0) continue;
				int v=(j+k)%m;
				ans[v]=(ans[v]+dpa[i][j]*dpb[i][k]%mod)%mod;
			}
		}
	}

但是wa掉了,只过了25%,太构思了,检查很多遍了。

不如先写F

这个提交非常不错,值得学习。

(0条未读通知) 代码查看

F题

给定一个长度为\(n\)的数组,给定\(q\)个操作,执行两种操作:

  • \(1\space x\space y\):将数组的第\(x\)为修改为\(y\)
  • \(2\space l\space r\):查询在区间\([l,r]\)内任取两个元素,其乘积是495的倍数的方案数。

很明显可以看到操作涉及单点修改区间查询,所以我们可以考虑使用树状数组来维护信息。对于区间内任选两个数,其乘积要是495的倍数,那么我们可以对495的所有因数开一个树状数组,维护这个因数的倍数的出现次数。但是会有一个问题,比如\(105=3\times 5\times 7\),既是3的倍数,又是5的倍数,也是15的倍数,那么这个数肯定只能够被算一次,所以我们取\(g=gcd(a[i],495)\)作为\(a[i]\)贡献的因数,这样保证每个数\(a[i]\)一定只会被计算一次,这样我们就可以直接做了。

预处理495的因数以及下标映射:

const int m=495;
//fac[i]表示m的第i个因数,pm[i]表示m的因数i在fac数组中的下标
vector<int> fac(13),pm(m+1);
int tot=0;//计数
for(int i=1;i<=m;i++){
    if(m%i==0){
        fac[++tot]=i;
        pm[i]=tot;
    }
}

初始化:

vecotr<Fenwick> tr(13,Fenwick(n));
for(int i=1;i<=n;i++){
    int g=gcd(a[i],m);
    tr[pm[g]].update(i,1);
}

对于\(update\)操作:我们先将原来位置\(x\)\(a[x]\)的贡献撤销,再将\(a[x]:=y\),再加上\(a[x]\)的贡献。

int g=gcd(a[x],m);
tr[pm[g]].update(x,-1);
a[x]=y;
g=gcd(a[x],m);
tr[pm[g]].update(x,1);

对于\(query\)操作:我们用\(cnt[i]\)表示第\(i\)个因数的贡献次数,求出\(cnt\)数组;之后我们枚举不同的因数组合\((i,j)\),判断\(m\mid fac[i]\times fac[j]\),然后求\(cnt[i]\times cnt[j]\)贡献即可。两个注意点:(1)注意枚举时\(i\leq j\),避免重复统计;(2)\(i=j\)时,贡献为\(cnt[i]\times(cnt[i]-1)/2\)

vector<int> cnt(13,0);
for(int i=1;i<=12;i++){
    cnt[i]=tr[i].query(r)-tr[i].query(l-1);
}
int ans=0;
for(int i=1;i<=12;i++){
    for(int j=i;j<=12;j++){
        int g=gcd(fac[i]*fac[j],m);
        if(g==m){
            if(i==j) ans+=cnt[i]*(cnt[i]-1)/2;
            else ans+=cnt[i]*cnt[j];
        }
    }
    cout << ans << endl;
}

这样就解决问题了,树状数组模板为:

struct Fenwick {
	vector<int> sum;
	int n;
	Fenwick(int size) : n(size), sum(size + 2, 0) {}
	int lowbit(int x){
		return x&-x;
	}
	void update(int pos,int x){
		for(int i=pos;i<=n;i+=lowbit(i))
			sum[i]+=x;
	}
	int query(int pos){
		int res=0;
		for(int i=pos;i;i-=lowbit(i))
			res+=sum[i];
		return res;
	}
	
	void init(){
		for(int i=0;i<=n;i++){
			sum[i]=0;
		}
	}
};

法二:线段树+状压

大致思路是线段树的\(node\)节点为:

struct segment{
    int l,r;
    vector<int> cnt;
    //3 3 5 11
    //假如i=13=(1101),也就表示是3*3*11倍数的数,cnt[i]表示这个状态的个数
    //当然这里也是和m的最大公因数
}

因为有状压,所以我们还需要一个\(encode\)函数,用来对\(a[i]\)进行转化为状态。\(pushup\)函数就将\(cnt\)数组合并就行,因为是单点修改,我们不需要\(pushdown\)函数了,\(update\)函数就是一样的先将\(a[x]\)的贡献撤销,然后使用\(encode\)\(y\)进行转换,然后加上贡献。

最关键的就是\(query\)函数了,这个和区间合并是一样的,每次我们返回的是一个\(vector<int>cnt\)数组。然后在\(op=2\)中,我们得到了\(cnt\)数组之后,在枚举状态\((i,j)\)时,需要注意这两个状态表示的数的乘积能否被\(m\)整除,然后和第一种方法一样,统计答案就好。

posted @ 2025-10-14 14:29  alij  阅读(17)  评论(0)    收藏  举报