# 競程班第四堂社課 [TOC] ## 開始之前 請先看過[這份講義](https://hackmd.io/@tcirc-40th/home/%2FIkrdjh4bQum_Ub1OtNP8xA)的vector部分 記得繳社費 ## 位元運算子 和[布林運算子](https://hackmd.io/@tmting39/rJdk5ioxp#%E5%B8%83%E6%9E%97%E9%81%8B%E7%AE%97%E5%AD%90)不一樣。 |$a$|$b$|$a\& b$|$a\|b$|$a^\wedge b$| |---|---|---|---|---| |0|0|0|0|0| |0|1|0|1|1| |1|0|0|1|1| |1|1|1|1|0| 位元運算是將兩個數字分別用二進位表示,位元分別進行運算。 例如$9_{(10)}=1001_{(2)}$,$5_{(10)}=101_{(2)}$,則$9_{(10)}|5_{(10)}=1101_{(2)}=13_{(10)}$。 ### 關於XOR 可以想成 看兩個位元一不一樣 或 沒有進位的加法。能應用在判斷奇偶性。 以下用$\oplus$表示位元XOR 有一個重要的性質 $$ a\oplus a =0 $$ 在數學上,我們會稱$0$是$\oplus$運算的**單位元素**(任何數對單位元素做運算後都不會變)。而一個數和他的**反元素**做運算後會變成單位元素,在XOR運算中,$a$的反元素正好就是$a$。 例如:若 $$ a\oplus b=c $$ 則 $$ a=c\oplus b $$ ## 對答案二分搜 就是帶答案,並縮小範圍 ### 範例 ![image](https://hackmd.io/_uploads/Sk_DPikNp.png) 若你懶得算,想要從選項代回去,可以從中間的開始代,再看要往小的猜還是大的猜。 先把選項排序: $\{1200,1500,1800,3000\}$。 選中間的開始猜,假設這裡先從1500開始,會發現: $$ 1500\times 0.5 < \frac{1}{2} \times 0.02 \times 300^2 $$ 這表示我們要往更大的數字猜。 :::warning 更好的例子是估計平方根 ::: --- [CSES Factory Machines](https://cses.fi/problemset/task/1620) [題解](https://hackmd.io/@tmting39/rJr8b_jza) ## 前綴和 ### 範例 [Static Range Sum Queries](https://cses.fi/problemset/task/1646) 題目大意: 給一個長度$N$的陣列$A$,及$Q$筆詢問,回答$\sum_{i=l}^{r} x_i$。 - $1 \le N,Q \le 2 \cdot 10^5$ - $1 \le A_i \le 10^9$ - $1 \le l \le r \le n$ ### 直覺做法 暴力回答詢問 ```cpp= #include<bits/stdc++.h> #define ll long long using namespacec std; int main(){ int n,q; cin>>q; vector<int> a(n); for(int i=0;i<n;i++) cin>>a[i]; while(q--){ ll ans=0; int l,r; cin>>l>>r; l--; r--; // 0-based for(int i=l;i<=r;i++) ans+=a[i]; cout<<ans<<endl; } return 0; } ``` 我們來分析一下複雜度,總共有$Q$筆詢問,而每次回答詢問最多都需要遍歷大小為$N$的陣列。所以總時間複雜度為$\text{O}(NQ)$,很明顯無法通過時限。 ### 觀察 可以先試著把問題簡化:假設所有的$a$都是$0$。 我們可以一開始就把所有的答案存到陣列裡(因為最多只會有$N$種不同的詢問),而這非常的容易。 可以開一個陣列$ans$,$ans_i$表示從$A_0$到$A_i$的和,馬上就可以發現$ans_i=ans_{i-1}+A_i$。所以可以在$\text{O}(N)$的時間完成預處理,總時間複雜度$\text{O}(N+Q)$。 接著來想想看當$a\neq 0$的時候該怎麼做 ![](https://i.imgur.com/u3EFzY2.png =400x240) $\sum_{i=l}^{r} a_i=\sum_{i=0}^{r}a_i-\sum_{i=0}^{l-1}a_i$。 藍色=黃色-綠色 ### 作法 對於一個數列$A$,定義$A_i$的前綴和$pre_i$為$\sum_{i=0}^{i} A_i$。 ```cpp= #include<bits/stdc++.h> #define ll long long using namespace std; int main(){ int n,q; cin>>n>>q; vector<ll> a(n),pre(n); for(int i=0;i<n;i++) cin>>a[i]; pre[0]=a[0]; for(int i=1;i<n;i++) pre[i]=pre[i-1]+a[i]; while(q--){ int l,r; cin>>l>>r; l--; r--; if(l==0) cout<<pre[r]<<endl; else cout<<pre[r]-pre[l-1]<<endl; } return 0; } ``` :::success 也不一定要再開一個新的陣列 可以直接做`for(int i=1;i<n;i++) a[i]+=a[i-1];` ::: ## 差分 ### 範例 有一個長度$N$的陣列$A$初始都是$0$,並且有$Q$筆操作$l$、$r$、$k$,將$A_l,A_{l+1},\cdots ,A_r$都加上$k$。問最後的陣列長怎樣。 - $1 \le N,Q \le 2 \cdot 10^5$ - $1 \le k \le 10^9$ - $1 \le l \le r \le n$ ### 觀察 對於一筆操作,會發現該區間中間的**趨勢**並沒有變。可以想成原本是一個平原,將某一個區間拉高成高原(或者反過來)。 ![](https://hackmd.io/_uploads/rktbUnj76.jpg) 若我們可以只記錄趨勢,那麼每一筆操作就只需要改兩個地方就好。 ### 作法 差分: 紀錄數列中兩個相鄰數字的差,也可以想成一個折線圖的**斜率**或**趨勢**。 簡單來講就是前綴和的逆運算 ![](https://hackmd.io/_uploads/S1T2NjoQa.png) 要將差分數列變為原數列只要做一次前綴和就好。 ```cpp= vector<int> dif(n),ans(n); while(q--){ int l,r,k; cin>>l>>r>>k; l--; r--; dif[l]+=k; if(r!=n-1) dif[r+1]-=k; } ans[0]=dif[0]; for(int i=1;i<n;i++) ans[i]=ans[i-1]+dif[i]; ``` ## 前綴和與差分的應用 若將數列改成連續的函式,前綴和以及差分各可以對應到積分和微分 ### [Maximum Subarray Sum](https://cses.fi/problemset/task/1643) 給一個陣列,求最大的子區間之和。 可以先預處理前綴和,然後窮舉子區間的左右界,再全部取max,但是這樣的作法是$\text{O}(n^2)$,所以需要一些優化。 ```cpp= int ans=0; for(int i=1;i<n;i++) pre[i]+=pre[i-1]; for(int i=0;i<n;i++){ //right ans=max(ans,pre[i]) // left=0; for(int j=0;j<i;j++){ //left-1,題目要求nonempty所以不能等於 ans=max(ans,pre[i]-pre[j]); } } ``` 對於每個右界,我們需要找一個最小的左界。所以我們可以額外紀錄每一個地方前面的最小前綴和,即令 $$ mn_i=\min_{0\leq j\leq i-1} pre_j $$ 發現到這件事也可以在$\text{O}(n)$內完成,因為可以改寫成 $$ mn_i=\min(mn_{i-1},pre_{i-1}) $$ 最後的答案即為 $$ \max_{0\leq i<n} (pre_i-mn_i) $$ ```cpp= #include<bits/stdc++.h> #define ll long long using namespace std; signed main(){ int n; cin>>n; vector<ll> pre(n),mn(n); for(int i=0;i<n;i++){ cin>>pre[i]; if(i!=0) pre[i]+=pre[i-1]; } ll ans=pre[0]; for(int i=1;i<n;i++){ mn[i]=min(mn[i-1],pre[i-1]); ans=max(ans,pre[i]-mn[i]); } cout<<ans<<endl; } ``` 其實也不一定要存成陣列 ```cpp= #include<bits/stdc++.h> #define ll long long using namespace std; signed main(){ int n; ll ans=-1e18,pre=0,mn=0; cin>>n; while(n--){ int a; cin>>a; pre+=a; ans=max(ans,pre-mn); mn=min(mn,pre); } cout<<ans<<endl; } ``` ## 習題 [Multiplication Table](https://cses.fi/problemset/task/2422) :::spoiler 解法: 對答案二分搜,尋找最小的$x$使得乘法表中小於等於$x$的數字超過$(n*n-1)/2$個,即$\sum_{i=1}^{n} \min{(n,\lfloor \frac{x}{i}\rfloor)} > \frac{n*n-1}{2}$ ::: :::spoiler code: ```cpp= #include<bits/stdc++.h> #define int long long using namespace std; int n; bool check(int x){ int cnt=0; for(int i=1;i<=n;i++){ cnt+=min(x/i,n); } return cnt>(n*n-1)/2; } signed main(){ cin>>n; int l=1,r=n*n+1,mid; if(n==1){ cout<<1<<endl; return 0; } while(r-l>1){ mid=(l+r)/2; if(check(mid)){ r=mid; } else{ l=mid; } } cout<<r<<endl; } ``` ::: --- [Forest Queries](https://cses.fi/problemset/task/1652) :::spoiler ans: ![image](https://hackmd.io/_uploads/rJDeib6Xa.png) 紅色=黑色-綠色-藍色+桃紅色 ::: :::spoiler code: ```cpp= #include<bits/stdc++.h> using namespace std; int main(){ int n,q; cin>>n>>q; vector<vector<int>> pre(n,vector<int>(n)); //int pre[n][n] for(int i=0;i<n;i++){ string s; cin>>s; for(int j=0;j<n;j++){ pre[i][j]=(s[j]=='*'); if(i>0) pre[i][j]+=pre[i-1][j]; if(j>0) pre[i][j]+=pre[i][j-1]; if(i>0&&j>0) pre[i][j]-=pre[i-1][j-1]; } } while(q--){ int a,b,c,d; cin>>a>>b>>c>>d; a--;b--;c--;d--; int ans=pre[c][d]; if(a>0) ans-=pre[a-1][d]; if(b>0) ans-=pre[c][b-1]; if(a>0&&b>0) ans+=pre[a-1][b-1]; cout<<ans<<endl; } } ``` ::: --- [Subarray Divisibility](https://cses.fi/problemset/task/1662/) :::spoiler 解法: 一個區間$[l,r]$的和能被$n$整除若且唯若$pre_{l-1}\equiv pre_{r} \pmod{n}$,換句話說,對於同餘的前綴和,隨意抓兩個出來都能夠組成一個能被$n$整除的區間 ::: :::spoiler code: ```cpp= #include<bits/stdc++.h> #define int long long using namespace std; signed main(){ int n; cin>>n; vector<int> v(n),cnt(n); for(int i=0;i<n;i++){ cin>>v[i]; if(i!=0) v[i]+=v[i-1]; v[i]%=n; cnt[(v[i]+n)%n]++; } cnt[0]++; int ans=0; for(int i=0;i<n;i++) ans+=((cnt[i]-1)*cnt[i])/2; cout<<ans<<endl; return 0; } ``` ::: --- [B. Karen and Coffee](https://codeforces.com/contest/816/problem/B) :::spoiler code: ```cpp= #include<bits/stdc++.h> using namespace std; signed main(){ int n,k,q; cin>>n>>k>>q; vector<int> cnt(2e5+2); // difference array of how many recipes recommand i degree while(n--){ int a,b; cin>>a>>b; cnt[a]++; cnt[b+1]--; } for(int i=1;i<2e5+2;i++) cnt[i]+=cnt[i-1]; // differnce array to original array for(int i=1;i<2e5+2;i++) cnt[i]=(cnt[i]>=k); // cnt[i]=1 if i degree is admissible for(int i=1;i<2e5+2;i++) cnt[i]+=cnt[i-1]; /* original array to prefix sums array there are cnt[i] admissible temperatures from 1 to i degrees*/ while(q--){ int a,b; cin>>a>>b; cout<<cnt[b]-cnt[a-1]<<endl; } } ``` ::: --- [A. Greg and Array](https://codeforces.com/contest/295/problem/A) :::spoiler code: ```cpp= #include<bits/stdc++.h> #define int long long using namespace std; signed main(){ int n,m,k; cin>>n>>m>>k; vector<int> ans(n),opcnt(m); vector<vector<int>> op(m,vector<int>(3)); //operation information for(int i=0;i<n;i++){ int a; cin>>a; ans[i]+=a; if(i!=n-1) ans[i+1]-=a; } for(int i=0;i<m;i++){ for(int j=0;j<3;j++){ cin>>op[i][j]; } op[i][0]--; op[i][1]--; } while(k--){ int a,b; cin>>a>>b; a--; b--; opcnt[a]++; if(b!=m-1) opcnt[b+1]--; } for(int i=1;i<m;i++) opcnt[i]+=opcnt[i-1]; /* difference array to original array the i-th operation will be applied opcnt[i] times */ for(int i=0;i<m;i++){ ans[op[i][0]]+=op[i][2]*opcnt[i]; if(op[i][1]!=n-1) ans[op[i][1]+1]-=op[i][2]*opcnt[i]; } cout<<ans[0]<<' '; for(int i=1;i<n;i++){ ans[i]+=ans[i-1]; cout<<ans[i]<<' '; } cout<<endl; return 0; } ``` ::: --- [B. Kuriyama Mirai's Stones](https://codeforces.com/contest/433/problem/B) :::spoiler code: ```cpp= #include<bits/stdc++.h> #define ll long long using namespace std; int main(){ int n; cin>>n; vector<ll> v(n); for(int i=0;i<n;i++) cin>>v[i]; vector<ll> u=v; sort(u.begin(),u.end()); for(int i=1;i<n;i++){ v[i]+=v[i-1]; u[i]+=u[i-1]; } int q; cin>>q; while(q--){ int t,l,r; cin>>t>>l>>r; l--; r--; if(t==1){ if(l==0) cout<<v[r]<<endl; else cout<<v[r]-v[l-1]<<endl; } else{ if(l==0) cout<<u[r]<<endl; else cout<<u[r]-u[l-1]<<endl; } } } ``` ::: --- [g597. 3. 生產線](https://zerojudge.tw/ShowProblem?problemid=g597) :::spoiler 解法: 先計算出每個位置的工作量,再安排機器的位置,越快的機器做越多事越好。 ::: :::spoiler code: ```cpp= #include<bits/stdc++.h> #define ll long long using namespace std; signed main(){ int n,m; cin>>n>>m; vector<int> v(n); while(m--){ int l,r,w; cin>>l>>r>>w; l--; r--; v[l]+=w; if(r!=n-1) v[r+1]-=w; } for(int i=1;i<n;i++) v[i]+=v[i-1]; //differnce array to original array sort(v.begin(),v.end()); vector<int> t(n); for(int i=0;i<n;i++) cin>>t[i]; sort(t.begin(),t.end()); reverse(t.begin(),t.end()); ll ans=0; for(int i=0;i<n;i++) ans+=t[i]*v[i]; cout<<ans<<endl; } ``` ::: --- [C. Little Girl and Maximum Sum](https://codeforces.com/contest/276/problem/C) :::spoiler 解法: 和上面那一題差不多 ::: --- [Range Xor Queries](https://cses.fi/problemset/task/1650/) :::spoiler 解法: ![image](https://hackmd.io/_uploads/S11SRW-VT.png) $$ 綠色\oplus 藍色=黃色 $$ 綠色和黃色可以先用前綴XOR處理,而答案就是 $$ 藍色=黃色\oplus 綠色 $$ ::: :::spoiler code: ```cpp= #include<bits/stdc++.h> using namespace std; int main(){ int n,q; cin>>n>>q; vector<int> pre(n); for(int i=0;i<n;i++){ cin>>pre[i]; if(i!=0) pre[i]^=pre[i-1]; } while(q--){ int a,b; cin>>a>>b; a--; b--; if(a==0) cout<<pre[b]<<endl; else cout<<(pre[b]^pre[a-1])<<endl; } } ``` ::: --- [2301 . 社交能量](https://tioj.ck.tp.edu.tw/problems/2301) :::spoiler code: ```cpp= #include<bits/stdc++.h> #define ll long long using namespace std; signed main(){ int n; ll ans=0; cin>>n; vector<ll> v(1e6+1); for(int i=0;i<n;i++){ int a,b; cin>>a>>b; v[a]++; v[b]--; } for(int i=1;i<=1e6;i++){ v[i]+=v[i-1]; if(v[i]>1) ans+=(v[i]-1)*v[i]/2; } cout<<ans<<endl; } ``` :::