【更新中】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 ,⽀持兩種操作:
- 向集合 S 種加⼊⼀個數 x;
- 對於⼀個數字 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) 被包含的區間數量)\)
Ans += Cal(j, d) * f[d];
20連測_Day9_B_運算元列
給出一個長度為 \(k\) 的數列,然後給出 \(n\) 個操作,操作分為三種:
- a[i]=b
- a[i]+=b
- 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;
}