【更新中】2021ZR模拟赛要题记录

ZR模拟赛要题记录

(NOIP十连测、考前二十连测)

姊妹篇:ZR模拟赛未了的心愿

NOIP_Day5_合并石子(Continuing)

将长度都为 n 的 a 和 b 两列石子等概率一一配对求和,求第 k 大期望的石子个数。

求第 K 大的期望,即求出对于任意 \(x\), 第 K 大的数等于 x 的期望,求和即为答案。

对于每一个 x 来说:

为了求出第 K 大等于 x,我们设:

f_i 表示至少有 i 对小于等于 x 的方案数,
g_i 表示恰好有 i 对小于等于 x 的方案数。

根据二项式反演,有:

g_i = \sum_{j_i} ^ {j <= n} f_j * \binom{j}{i}

第 K 大的数小于等于 x $\Leftrightarrow $ 存在某个 \(k \leq K\), 恰好有 k - 1 个数大于 x ,其他的数都小于等于x。

dp_{i,j} 表示匹配了a的前i个数,至少存在j对和小于等于x

20连测_Day1_多项式题

设d_i表示前i位的划分权值和,也就是字串[1,i]的答案,相当于从i和i+1之间断开。

发现这种乘积加和满足乘法分配律,也就是对于划分出来的数字[i,j],\(d_j = d_{i - 1} * num_{i,j}\),我们并不关心前j位是怎么划分的,而可以直接把\(num_{i,j}\)乘到后面。

\(d_i = \sum_{j = 1} ^ {j < i} d_j * num_{j + 1, i} = \sum_{j = 1} ^ {j < i} (d_j * (mulum[i] - mulum[j] * 10^{i - j})) = \sum(d_j * mulum[i] - d_j * mulum[j] * 10^{i - j})\)

倒序枚举 j 那么\(10^{i-j}\)可以累乘。

前缀和优化, 记:
\( D_i = \sum_{j = 1} ^ {j <= i} d_i \\ Sj_i = \sum_{j = 1} ^ {j <= i} mulum[j] * Teninv[j] * d[j] \)

那么 \(d_i = mulum[i] * D_{i - 1} - Ten[i] * Sj_{i - 1}\)

复杂度:\(O(n)\)

int n;
ll d[maxn], mulum[maxn], Ten[maxn], D[maxn], Sj[maxn], Teninv[maxn], inv10;
char s[maxn];
ll ksm(ll bs, int B){
	ll res(1);
	while(B){
		if(B & 1){
			res = res * bs % Mod;
		}
		bs = bs * bs % Mod;
		B >>= 1;
	}
	return res;
}
int main(){
	n = rd();
	scanf("%s", s + 1);
	Teninv[0] = D[0] = Ten[0] = d[0] = 1ll; inv10 = ksm(10, Mod - 2);	
	for(int i(1); i <= n; ++i) Ten[i] = Ten[i - 1] * 10 % Mod, Teninv[i] = Teninv[i - 1] * inv10 % Mod, mulum[i] = (mulum[i - 1] * 10 + s[i] - '0') % Mod;
	for(int i(1); i <= n; ++i){
		d[i] = (D[i - 1] * mulum[i] % Mod - Sj[i - 1] * Ten[i] % Mod + Mod) % Mod;
		D[i] = D[i - 1] + d[i]; Sj[i] = Sj[i - 1] + mulum[i] * Teninv[i] % Mod * d[i] % Mod;
		if(D[i] >= Mod) D[i] -= Mod; if(Sj[i] >= Mod) Sj[i] -= Mod; 
	}
	printf("%lld\n", d[n]);
	return 0;
}

20连测_Day2_数集

你需要维护⼀个⼀开始为空的⾮负整数集 S ,⽀持两种操作:

  1. 向集合 S 种加⼊⼀个数 x;
  2. 对于⼀个数字 y ,查询\(\max_{x\in S} x op y\) ,其中 op 是与、或、异或三种运算中的某⼏种。
    (x 值域是 \(1 ^ 20\)
  • op = xor

用 trie 树维护集合即可。

  • op = and

从高位向低位,如果 y 当前位为1,那么我们希望选择这一位为1的数字;

如果 y 当前位为0,无法做出决定。
每当我们遇到无法做出决定的时刻,就不做出决定,也即默认这一位为0。

  • op = or

从高位向低位,如果 y 当前位为0,那么我们希望选择这一位为1的数字;
如果 y 当前位为1,无法做出决定,只能为0。

枚举子集

枚举所有的正整数 i ,使得 \(i | x = x\),方法如下:

for(int i = x; i; i = (i - 1) & x);

注意这里的边界条件是 i != 0, 而不是 i >= 0,如果 i = 0 了, 那么负数做 & 运算会出大问题。

每插入一个数 x, 我们把 vis[] 中 x 的所有子集 i 都置为1,代表与集合 S 内与 y 的按位与、按位或结果可能是 i。

注意: trie 树要开值域乘2,因为 trie 树上第 i 层有 \(2 ^ i\) 个结点,\(\sum_{i=0}^{20} = 2 ^ 21 - 1\)

代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int rd(){
	int res = 0, fl = 1; char c = getchar();
    while(!isdigit(c)){if(c == '-') fl = -1; c = getchar();}
    while(isdigit(c)){res = (res << 3) + (res << 1) + c - '0'; c = getchar();}
    return res * fl;
}
const int maxn = 1050010;
int ans1, ans2, q, op, x, trie[2][maxn * 20], cnt, vis[maxn];
int Max(int A, int B){
	if(A < B) return B;
	return A;
}
void insert(int num){
	int k = 0;
	for(int i(20); i >= 0; --i){
		int b1 = (num >> i) & 1;
		if(!trie[b1][k]) trie[b1][k] = ++cnt;
		k = trie[b1][k];
	}
} 
int queryxor(int num){
	int k(0), res(0);
	for(int i(20); i >= 0; --i){
		int b1 = (num >> i) & 1;
		if(trie[!b1][k]){
			res += (1 << i);
			k = trie[!b1][k];
		}
		else k = trie[b1][k];
	}
	return res;
}
int main(){
//	freopen("B2.in", "r", stdin);
	q = rd();
	for(int i(1); i <= q; ++i){
		op = rd(), x = rd();
		if(op == 1){
			insert(x); 
			if(!vis[x])
				for(int i(x); i; i = (i - 1) & x)	vis[i] = 1;
			continue; 
		}
		if(op == 3){
			printf("%d\n", queryxor(x)); continue;
		}
		ans1 = ans2 = 0;
		for(int i(20); i >= 0; --i){
			if(x & (1 << i)){//当前为1,&希望为1,| 无所谓 
				if(vis[ans1 | (1 << i)]) ans1 |= (1 << i);
			}
			//当前为0,& 无所谓,|希望为1 
			else if(vis[ans2 | (1 << i)]) ans2 |= (1 << i);
		}
		printf("%d %d %d\n", queryxor(x), ans1, ans2 | x);
	}
	return 0;
}

20连测_Day2_染色

20连测_Day3_A

预处理 popcount

我的方法:

for(int i(1);i<len;++i){pct[i]=pct[i-lowbit(i)]+1;}

老师的方法:

for(int x(1);x<=n;++x) popct[x] = popct[x >> 1] + (x & 1);

内存过大,数组访问不连续,不如O(n)跑函数

以下写法在 n=25,m=291 的极限数据情况下,运行时间超过了2000ms。

unsigned pct[35000000];
inline void dfs(unsigned pos,unsigned cnt,unsigned sta){
	if(pos==n){++ans[cnt]; return;}
	dfs(pos+1,cnt,sta);
	dfs(pos+1,cnt+pct[sta&G[pos]],sta|(1<<pos));
}
int main(){
	...
	for(unsigned i(1);i<(1<<n);++i) pct[i]=pct[i>>1]+(i&1);
	dfs(0,0,0);
	...
}

而以下写法同样的数据只花费了324ms。

inline int pct(unsigned x){
	unsigned res(0);
	for(;x;x-=lowbit(x)) res++;
	return res;
}
inline void dfs(unsigned pos,unsigned cnt,unsigned sta){
	if(pos==n){++ans[cnt]; return;}
	dfs(pos+1,cnt,sta);
	dfs(pos+1,cnt+pct(sta&G[pos]),sta|(1<<pos));
}
int main(){
	dfs(0,0,0);
}

这是因为 pct 数组的大小有3.5e7,内存访问不连续,所以慢到理论上的\(O(1)\)查询还不如写一个函数来 \(O(n)\) 求。

20连测_Day3_B[莫反板题]

\(\sum_{i=l}^{i<=r}[gcd(a_i,x)==1] \\= \sum_{i=l}^{i<=r}\sum_{d|gcd(a_i,x)}\mu(d) \\= \sum_{i=l}^{i<=r}\sum_{d|x,d|d_i}\mu(d) \\= \sum_{d|x}\sum_{i=l}^{i<=r}[d|a_i]*\mu(d)\)

这时候的神仙操作就是先给所有询问按照 r 排序,然后总的均摊 O(n) 处理所有 r 前面的 a[i]。

更神仙的是对于左端点 l ,只需要给 l-1 乘一个 -1 的系数,转化为 r,一并排序,然后在最终输出答案的时候再把 r 得到的答案减去 l 得到的答案即可。

对于 \(d|a_i\) 相当于是我们要找一个数 d 的倍数的出现次数。

那么处理a[i]的操作就是\(O(\sqrt(a))\)算出他是那些数的倍数即可。

\(\mu(x)=1 if (x == 1);\\=(-1)^k if (x 无平方因数且 x=\Pi_{i=1}^{i<=k}p_i)\\0 else\)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int rd(){
	int res = 0, fl = 1; char c = getchar();
    while(!isdigit(c)){if(c == '-') fl = -1; c = getchar();}
    while(isdigit(c)){res = (res << 3) + (res << 1) + c - '0'; c = getchar();}
    return res * fl;
}
const int maxn = 100010;
int mu[maxn];
unsigned not_pri[maxn], pri[maxn], top, q, n, m, l, r, x, a[maxn], cnt[maxn], ans[maxn];
unsigned sum[1010][maxn];
map<pair<int, int>, int> Cnt;
int Gcd(int A, int B){
	return B ? Gcd(B, A % B) : A;
}
int cal(int x){
	int i, ans(0);
	for(i=1;i*i<x;++i){
		if(!(x%i)){
			ans+=mu[i]*cnt[i], ans+=mu[x/i]*cnt[x/i];
		} 
	} 
	if(i*i==x){
		ans+=mu[i]*cnt[i];
	} 
	return ans;
}
void getmu(){
	mu[1]=1;
	for(int i(2);i<=100000;++i){
		if(!not_pri[i]){
			pri[++top]=i; mu[i]=-1;
		}
		for(int j(1);j<=top&&i*pri[j]<=100000;++j){
			not_pri[i*pri[j]]=1;
			if(i%pri[j]==0){
				mu[i*pri[j]]=0;break;
			}
			mu[i*pri[j]]=-mu[i];
		}
	}
}
struct Query{
	int r,x,id,t;
	Query(){}
	Query(int R,int X,int Id,int T){
		r=R,x=X,id=Id,t=T;
	}
	bool operator < (Query Q) const {
		return this->r < Q.r;
	}
}qry[maxn*2];
int main(){
	getmu();
	n = rd(),m = rd();
	for(int i(1); i <= n; ++i) a[i]=rd();
	for(int i(1); i <= m; ++i){
		l=rd()-1,r=rd(),x=rd();
		qry[++q]=Query(l,x,i,-1), qry[++q]=Query(r,x,i,1);
	}
	sort(qry+1,qry+1+q);
	int c1(1);
	for(int i(0);i<=n;++i){
		int x;
		for(x=1;x*x<a[i];++x){
			if(!(a[i]%x)) cnt[x]++,cnt[a[i]/x]++;
		}
		if(x*x==a[i]) cnt[x]++;
		while(c1<=q&&qry[c1].r<=i){
			ans[qry[c1].id]+=qry[c1].t*cal(qry[c1].x);
			c1++;
		}
	}
	for(int i=1;i<=m;++i) printf("%d\n",ans[i]); printf("\n");
	return 0;
}
//sqrt(100000)=316 3e7
//pri_100000 = 1e4
//1e5*1e4/64=1.6e7 

NOIp_Day6_买

我在NOIp模拟赛中做小学奥数原题爆零了

一次买一个,慢死。

一次买 \(\rm a\) 个直到钱不够 \(\rm a * x\),有点慢。

\(\rm a\) 个得到 \(b\) 元钱,实际花费 \(\rm (a*x-b)\) 元钱,如果 \(a * x - b <= 0\),我就可以无限买。

\(n/(a*x-b)\) 表示买多少个 \(a\),但是当我的钱不够买 \(\rm a\) 个但是却能花费 \(\rm a * x - b\) 时是不能买的,所以先买 \((n-a*x)/(a*x-b)\) 个。【1】

可以确定的是

那么现在我还有 \(n=(n-a*x)\mod(a*x-b)+a*x\) 元钱,考虑我肯定至少还能买一开始保底的 \(a\) 个,而且还有剩钱。

\(rest=(n-a*x)\mod(a*x-b)\) 在 $$

我能否买 \(2*a\) 个? \(3*a\) 个呢? 说不定可以,我买 \(a\) 还能增加 \(\rm b\) 元钱,加上我剩的钱,我还能进行一波【1】操作。

最后剩的钱绝对凑不够 \(a * x\) 就只能单买了。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int rd(){
	int res = 0, fl = 1; char c = getchar();
    while(!isdigit(c)){if(c == '-') fl = -1; c = getchar();}
    while(isdigit(c)){res = (res << 3) + (res << 1) + c - '0'; c = getchar();}
    return res * fl;
}
int T;
ll ans, a, b, x, n, money;
int main(){
	T=rd();
	for(int t(1);t<=T;++t){
		ans=0;n=rd(),x=rd(),a=rd(),b=rd();
		if(n<x){
			printf("0\n"); continue;
		}
		if(n>=x*a&&a*x<=b){
			printf("-1\n"); continue;
		}
		if(b==0){
			ans=n/x;
			printf("%lld\n",ans); continue;
		}
		if(n<a*x){
			printf("%lld\n",n/x); continue;
		}
		ll l=(n-a*x)/(a*x-b);
		money=n-l*(a*x-b);
		ll l_=money%(a*x);
		ll l__=l_/(a*x-b)+1;
		money-=l__*(a*x-b);
		ans=l*a+l__*a+money/x;
		printf("%lld\n", ans);
	}
	return 0;
}
/*
5
100000000 2 1 1
1000000000 100 2 99
6651238 99 2 186
100000000 100000000 2 100000001
594580363 1 249195783 241474592
*/

NOIp_Day7_C

\(设 f(x)=\sum_{d|x} (\mu(\frac{x}{d}) * d)\)

\(那么 gcd(x, y)=\sum_{d|x,d|y} f(d)\)

\(gcd(x,y)=\sum_{d|x,d|y}(\sum_{g|d}(\mu(\frac{d}{g}) * g))\)

证明:

不会.

枚举 \(\rm j\), 枚举 \(a_j\) 的因子 \(\rm d\), 这样对于每一个 \(\rm i\), 如果 \(a_i\)\(\rm d\) 这个因子就会对答案有贡献.

记录 \(f[d] = \sum_{g|d} \mu(\frac{d}{g}) * g, O(n\sqrt{n})\)

维护 \(sum[d]=\sum_{i \leq j \And d|a[i] \And d|a[j]} i\)

维护 \(squm[d]=\sum_{i \leq j \And d|a[i] \And d|a[j]} i*i\)

Cal() 计算\(\sum_{i}( (i,j) 被包含的区间数量)\)

\[(i-1)*(i-2)/2+i-1 + [(n-j)*(n-j-1)/2+n-j]\ast cnt + [(j-i-1)*(j-i-2)/2+j-i-1] = i*i/2-i/2 + [n*(n-1)/2+(1-j-n*2)*j/2]*cnt + j*j/2-i*j+i*i/2-3*j/2+3*i/2+1+j-i-1 = i*i + j*j/2 - i*j - (1/2+cnt/2)*j - n*cnt*j - 1 \]

Ans += Cal(j, d) * f[d];

20连测_Day9_B_操作数列

给出一个长度为 \(k\) 的数列,然后给出 \(n\) 个操作,操作分为三种:

  1. a[i]=b
  2. a[i]+=b
  3. a[i]*=b
    其中 \(i,b\) 是给定的,每个操作只能用一次,最多使用 \(m\) 个操作,让整个数列的乘积最大。

实际操作

  • 覆盖操作:对于每一个 \(a[i]\) 最多进行一次覆盖操作,也就是那个最大的 \(b\)
  • 增加操作:增加操作一定是在覆盖操作之后进行的,不然就白覆盖了。优先选择 \(b\) 大的加操作
  • 乘操作:乘操作一定放在加操作之后,优先选择 \(b\) 大的乘操作

思考实际上的操作过程,对于每一个 \(a_i\),一开始可能有一次覆盖操作,也可能没有,这要看覆盖操作自己的本身,接下来进行的是一些 \(b\) 单调不增的加操作,最后是一些 \(b\) 单调不增的乘操作。

连续!连续!前缀!前缀!

由于最终的答案很大,所以没法按照一次操作结束后 \(a\) 的值进行排序,但是按照操作后的数与操作前的数的比值进行排序是完全没问题的。
\(Pi=\Pi_{i=1}^{k} a_i\),考虑进行一次乘操作之后,\(Pi\) 要乘上 \(b\)
进行一次加操作后,\(Pi\) 变成 \(Pi+b\),相当于乘上了 \(\frac{Pi+b}{Pi}\)
我们考虑把覆盖转化为加操作,覆盖 \(b\) 变成加 \(b-a\)

这样对 \(a_i\) 的三种操作产生的实际效果都转化成为了对 \(Pi\) 的乘法。

乘标记——实际操作的提炼与转化

考虑所有的加操作(包括由覆盖操作转化过来的),按照 \(b\) 递减排序,从前往后扫,维护 \(sum_i\)\(sum_i\) 一开始等于 \(a_i\) 那么当前操作的乘标记即为 \(\frac{sum_i+b}{sum_i}\),然后把 \(b\) 累加到 \(sum_i\) 里面。

原先乘法的乘标记就是 \(b\)

之所以我们能够记录一个 \(sum_i\) ,都基于操作的单调性和连续性,一旦我们进行了当前操作,意味着差值比当前操作大的那些操作一定会进行。

贪心

排序,选乘标记大于1的前 \(m\) 个。

#include<bits/stdc++.h>
#define Mod 1000000007
using namespace std;
typedef long long ll;
int rd(){
	int res = 0, fl = 1; char c = getchar();
    while(!isdigit(c)){if(c == '-') fl = -1; c = getchar();}
    while(isdigit(c)){res = (res << 3) + (res << 1) + c - '0'; c = getchar();}
    return res * fl;
}
const int maxn = 1000010;
struct Add{
	double Tmp;
	ll del;
	int id;
}add[maxn];
struct MUL{
	ll Mul;
	int id;
}mul[maxn];
int k, n, m, T, I, tot, top, cnt, Tot;
ll a[maxn], cov[maxn], Sum[maxn], ans(1), B;
bool CmpAdd(Add A, Add B){
	return A.del>B.del;
}
bool CmpTmp(Add A, Add B){
	return A.Tmp>B.Tmp;
}
struct Op{
	double Tmp;
	int id, tp, pos;
}Ans[maxn];
bool operator < (Op A, Op B){
	return A.Tmp < B.Tmp;
}
priority_queue<Op> Q;
int main(){
	k=rd(), n=rd(), m=rd();
	for(int i(1);i<=k;++i) a[i]=rd();
	for(int i(1);i<=n;++i){
		T=rd(),I=rd(),B=rd();
		if(T==1) cov[I]=max(cov[I],B);
		else if(T==2) add[++tot].del=B, add[tot].id=I;
		else mul[++top]={B,I};
	}
	for(int i(1);i<=top;++i) Q.push(Op{mul[i].Mul,mul[i].id,3,i});
	for(int i(1);i<=k;++i) if(cov[i]){
		add[++tot].del=cov[i]-a[i], add[tot].id=i;
	}
	sort(add+1,add+1+tot,CmpAdd);
	for(int i(1);i<=k;++i) Sum[i]=a[i]; 
	for(int i(1);i<=tot;++i){
		add[i].Tmp=1.0*(add[i].del+Sum[add[i].id])/Sum[add[i].id];
		Q.push(Op{add[i].Tmp, add[i].id, 2, i}); 
		Sum[add[i].id]+=add[i].del;
	}
	while(Q.size()){
		Op u=Q.top(); Q.pop();
		if(u.Tmp <= 1) break;
		cnt++;
		Ans[++Tot]=u;
		if(cnt>=m) break; 
	}
	for(int i(1);i<=Tot;++i){
		if(Ans[i].tp==2){
			a[Ans[i].id]=(a[Ans[i].id]+add[Ans[i].pos].del)%Mod;
		}
	}
	for(int i(1);i<=Tot;++i){
		if(Ans[i].tp==3){
			a[Ans[i].id]=a[Ans[i].id]*mul[Ans[i].pos].Mul%Mod;
		}
	}
	for(int i(1);i<=k;++i) ans=ans*a[i]%Mod;
	printf("%lld\n", ans);
	return 0;
}
posted @ 2021-10-27 19:07  _Buffett  阅读(86)  评论(0编辑  收藏  举报