1. 程式人生 > 其它 >【更新中】2021ZR模擬賽要題記錄

【更新中】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;
}