---
# System prepended metadata

title: 競程班第四堂社課

---

# 競程班第四堂社課
[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;
}
```
:::


