1. 程式人生 > 實用技巧 >【ACM模板】

【ACM模板】

一、雜項

1.1 快讀

一般情況下不會卡 scanf

洛谷偷過來的一份檔案流快讀

class QIO {
public:
	char buf[1 << 21], * p1 = buf, * p2 = buf;
	inline int getc() {
		return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++;
	}
	int read() {
		int ret = 0, f = 0;
		char ch = getc();
		while (!isdigit(ch)) {
			if (ch == '-')
				f = 1;
			ch = getc();
		}
		while (isdigit(ch)) {
			ret = ret * 10 + ch - 48;
			ch = getc();
		}
		return f ? -ret : ret;
	}
	char Buf[1 << 21], out[20];
	int P, Size = -1;
	inline void flush() {
		fwrite(Buf, 1, Size + 1, stdout);
		Size = -1;
	}
	void write(int x, char ch = ' ') {
		if (Size > 1 << 20) flush();
		if (x < 0) Buf[++Size] = 45, x = -x;
		do {
			out[++P] = x % 10 + 48;
		} while (x /= 10);
		do {
			Buf[++Size] = out[P];
		} while (--P);
		Buf[++Size] = ch;
	}
} io;

__int128

一般情況不會用到 read, 比較常用 print

inline __int128 read(){
    __int128 x = 0, f = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9'){
        if(ch == '-')
            f = -1;
        ch = getchar();
    }
    while(ch >= '0' && ch <= '9'){
        x = x * 10 + ch - '0';
        ch = getchar();
    }
    return x * f;
}
inline void print(__int128 x){
    if(x < 0)putchar('-'), x = -x;
    if(x > 9)print(x / 10);
    putchar(x % 10 + '0');
}

1.2 大數

java

//A+B
import java.io.BufferedInputStream;
import java.math.BigInteger;
import java.util.Scanner;

public class Main{
    public static void main(String[] args){
        Scanner in = new Scanner(new BufferedInputStream(System.in));
        BigInteger a = in.nextBigInteger();
        BigInteger b = in.nextBigInteger();
        // BigInteger.ONE;
        // BigInteger.ZERO;
        // BigInteger.TEN;
        a = a.add(b);
        System.out.println(a);
    }
}

python

大數開根號

    from math import *
    from decimal import *

    getcontext().prec= 300 #設定有效位數
    def sqrt(n):
        return floor(Decimal(n).sqrt())
    def judge(n):
        t = sqrt(n)
        return t * t == n

1.3 模擬演算法

模擬退火

  1. 爬山演算法:兔子朝著比現在高的地方跳去。它找到了不遠處的最高山峰。但是這座山不一定是珠穆朗瑪峰。這就是爬山演算法,它不能保證區域性最優值就是全域性最優值。
  2. 模擬退火:兔子喝醉了。它隨機地跳了很長時間。這期間,它可能走向高處,也可能踏入平地。但是,它漸漸清醒了並朝最高方向跳去。這就是模擬退火。

根據熱力學規律並結合計算機對離散資料的處理, 我們定義: 如果當前溫度為 \(T\), 當前狀態與新狀態之間的能量差為 \(ΔE\), 則發生狀態轉移的概率為:

\[P(\Delta E) = e^{\dfrac {\Delta E} {T}} \]

\(\Delta E\) 為正的時候一定是轉移成功的,對於 $\Delta E < 0 $ , 則通過計算的概率接受這個新的解

可以寫出如下一段虛擬碼

void mnth(){ //模擬退火...
	for(double T = 初始溫度;T > 終止溫度;T *= 係數){
		rand_operate();//做一個隨機操作
		now = cal(); //計算目前的答案
		if(now < ans) ans = now;//若更優,直接轉移
		else if(exp((ans - now) / T) * RAND_MAX < rand()){
            //這裡寫的是小於號,是不轉移
			//不轉移,撤銷剛剛的隨機操作
		}
	}
}

看兩道題

P1337 [JSOI2004]平衡點 / 吊打XXX

其實就是選一個點,使得這個點到所有點的距離乘上質量之和要最小

相當於在二維的平面找一個最優解

#include<bits/stdc++.h>
#define N 2000
using namespace std;

struct node
{
	double x, y, w;
}e[N];
int n;
double ansx, ansy;
const double eps = 1e-15;
double f(double x, double y)
{
	double tot = 0;
	for (int i = 1; i <= n; i++){
		double delx = x - e[i].x;
		double dely = y - e[i].y;
		tot += sqrt(delx * delx + dely * dely) * e[i].w;
	}
	return tot;
}
void mnth()
{
	for(double T = 5000;T > 1e-15;T *= 0.995){
		double nowx = ansx + (rand() * 2 - RAND_MAX) * T;
		double nowy = ansy + (rand() * 2 - RAND_MAX) * T;
		double delta = f(nowx, nowy) - f(ansx, ansy);
		if (delta < 0)ansx = nowx, ansy = nowy;
		else if (exp(-delta / T)> rand()*1.0/RAND_MAX)ansx = nowx, ansy = nowy;
	}
}
int main(){
	scanf("%d", &n);
	for (int i = 1; i <= n; i++){
		scanf("%lf%lf%lf", &e[i].x, &e[i].y, &e[i].w);
		ansx += e[i].x; ansy += e[i].y;
	}
	ansx /= (double)n; ansy /= (double)n;
	mnth();
	printf("%.3lf %.3lf\n", ansx, ansy);
}

P3878 [TJOI2010]分金幣

現在有 \(n\) 枚金幣,它們可能會有不同的價值,第 \(i\) 枚金幣的價值為 \(v_i\)

現在要把它們分成兩部分,要求這兩部分金幣數目之差不超過 1 ,問這樣分成的兩部分金幣的價值之差最小是多少?

每次隨機交換兩個元素

當時這裡的小於號和大於號弄混了,調了好久

#include<bits/stdc++.h>
using namespace std;

int ans, T, n, A[50];

int get() {
	int sum = 0;
	for (int i = 0; i < n; i++) {
		if (i < n / 2)sum += A[i];
		else sum -= A[i];
	}
	return abs(sum);
}

void mnth() {
	for (double T = 5000; T > 1e-15; T *= 0.945) {
		int x = rand() % n, y = rand() % n;
		swap(A[x], A[y]); int now = get();
		if (now < ans) ans = now;
		else if (exp((ans - now) / T) * RAND_MAX < rand()) swap(A[x], A[y]); // > 是轉移, < 是撤銷
	}
}

int main() {
	scanf("%d", &T);
	while (T--) {
		scanf("%d", &n);
		for (int i = 0; i < n; i++)scanf("%d", A + i);
		ans = 0x7fffffff; int x = 1000; while (x--)mnth();
		printf("%d\n", ans);
	}
}

[P4035 [JSOI2008\]球形空間產生器 - 洛谷 ](https://www.luogu.com.cn/problem/P4035)

題意

\(n\) 維球體上的 \(n+1\) 個點,確定圓心座標

/*
 * @Author: zhl
 * @Date: 2020-01-21 17:30:34
 */
#include<bits/stdc++.h>
using namespace std;

double p[30][30], ans[30], mx, now[30];
int n;
double sqr(double x) {
	return x * x;
}
double cal(double* ar) {
	double mx = 0;
	double r = 0;
	for (int i = 1; i <= n; i++) {
		r += sqr(p[0][i] - ar[i]);
	}
	for (int i = 1; i <= n; i++) {
		double x = 0;
		for (int j = 1; j <= n; j++) {
			x += sqr(p[i][j] - ar[j]);
		}
		mx += sqr(x - r);
	}
	return mx;
}
int main() {
	scanf("%d", &n);
	for (int i = 0; i <= n; i++) {
		for (int j = 1; j <= n; j++) {
			scanf("%lf", &p[i][j]);
		}
	}
	mx = 1e18;
	for (double T = 5000; T > 1e-18; T *= 0.99995) {
		for (int i = 1; i <= n; i++) {
			now[i] = ans[i] + (rand() * 2 - RAND_MAX) * T;
		}

		double delta = cal(now) - cal(ans);
		if (delta < 0) {
			for (int i = 1; i <= n; i++)ans[i] = now[i];
		}
		else if (exp(-delta / T) > rand() * 1.0 / RAND_MAX) {
			for (int i = 1; i <= n; i++)ans[i] = now[i];
		}
	}
	for (int i = 1; i <= n; i++) {
		printf("%.3f ", ans[i]);
	}
}

爬山演算法

1.4 分數規劃

1.5 雜項

線性預處理

/*
 * @Author: zhl
 * @Date: 2020-10-12 21:04:36
 */
#include<bits/stdc++.h>
using namespace std;

const int N = 1e6 + 10;
int fac[N],invfac[N],inv[N];
const int mod = 998244353;

void init(){
    fac[0] = invfac[0] = 1;
    fac[1] = invfac[1] = 1;
    inv[1] = 1;
    for(int i = 2;i < N;i++){
        fac[i] = fac[i-1] * i % mod;
        inv[i] = (mod - mod / i)*inv[mod % i] % mod;
        invfac[i] = invfac[i-1] * inv[i] % mod;
    }
}

int C(int n,int m){
    return fac[n]*invfac[n-m] % mod *invfac[m] % mod;
}

約瑟夫環

求 n 個人玩,間隔 k, 第 m 個出圈的人

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f3f;
const ll mod = 1e9 + 7;
const int N = 1000;
ll cal1(ll n, ll m, ll k) {
    ll p = m % (n - k + 1);
    if (p == 0) p = n - k + 1;
    for (ll i = 2; i <= k; i++) {
        p = (p + m - 1) % (n - k + i) + 1;
    }
    return p;
}
ll cal2(ll n, ll m, ll k) {
    if (m == 1) return k;
    else {
        ll a = n - k + 1, b = 1;
        ll c = m % a, x = 0;
        if (c == 0) c = a;
        while (b + x <= k) {
            a += x, b += x, c += m * x;
            c %= a;
            if (c == 0) c = a;
            x = (a - c) / (m - 1) + 1;
        }
        c += (k - b) * m;
        c %= n;
        if (c == 0) c = n;
        return c;
    }
}
ll n, m, k, ans;
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    int t;
    cin >> t;
    for (int i = 1; i <= t; i++) {
        cin >> n >> m >> k;
        if (m < k)ans = cal1(n, k, m);
        else ans = cal2(n, k, m);
        cout << "Case #" << i << ": " << ans << endl;
    }
    return 0;
}

三元環計數

三元環計數

參考部落格

洛谷模板

無向圖三元環計數

將無向圖轉化成有向圖,度大的指向度小的,若度一樣,按照編號排序。

列舉每個點x,將x的所有相鄰點標記,然後列舉x的相鄰點y,再列舉y的相鄰點z,
如果z已經被標記,那麼(x,y,z)就是如圖示的三元環。

複雜度 : \(O(n\sqrt n)\)

#include<bits/stdc++.h>
using namespace std;

#define rep(i,a,b) for(int i = a;i <= b;i++)
#define repE(i,u) for(int i = head[u];i;i = E[i].next)

int n, m;

const int N = 1e5 + 10;
const int M = 2e5 + 10;

struct Edge {
	int to, next;
}E[M];
int head[N], tot;
void addEdge(int from, int to) {
	E[++tot] = Edge{ to,head[from] };
	head[from] = tot;
}
int deg[N], s[M], t[M];
int vis[N];

int main() {
	scanf("%d%d", &n, &m);
	for (int i = 0; i < m; i++) {
		scanf("%d%d", s + i, t + i); deg[s[i]]++; deg[t[i]]++;
	}
	for (int i = 0; i < m; i++) {
		int u = s[i], v = t[i];
		if (deg[u] == deg[v] and u < v)swap(u, v);
		if (deg[u] < deg[v])swap(u, v);
		addEdge(u, v);
	}
	int ans = 0;
	rep(u, 1, n) {
		repE(i, u) vis[E[i].to] = u;
		repE(i, u) {
			int to = E[i].to;
			repE(j, to) {
				int v = E[j].to;
				if (vis[v] == u) {
					ans++;
				}
			}
		}
	}
	printf("%d\n", ans);
}

1.6 注意

memset

memset 是按位元組初始化, memset(dis,0x7fffffff,sizeof dis) 這種寫法是錯誤的,實際上只會取最低的一個位元組 0xff , 所以相當於全部置成 0xffffffff ,就是 -1

第二個引數的值應該是 0-255 (0x00 - 0xff)

priority_queue

預設是大頂堆,每次取最大的值,可以用 greater<> 變成小頂堆

二、基礎演算法

我不會

三、搜尋

我也不會

四、字串

4.1 KMP

\(O(n)\) 的字串匹配

nxt陣列

nxt 陣列是對於匹配串 p 來說的

nxt[i] 表示 p.substr(0,i) 不包括自己 的最長公共字首字尾

A C A B
-1 0 0 0 1 2
/*
 * @Author: zhl
 * @LastEditTime: 2020-12-07 10:09:32
 */
#include<bits/stdc++.h>
using namespace std;
const int N = 2e6 + 10;
char s[N], p[N];
int nxt[N], n, m, ans[N], cnt;
void get_nxt() {
	nxt[0] = -1;
	for (int k = -1, j = 0; j < m;) {
		if (k == -1 or p[k] == p[j])nxt[++j] = ++k;
		else k = nxt[k];
	}
}
void kmp() {
	for (int j = 0, i = 0; i < n;) {
		if (j == -1 or p[j] == s[i]) {
			i++, j++;
			if (j == m)j = nxt[j], ans[++cnt] = i - m;
		}
		else j = nxt[j];
	}
}
int main() {
	scanf("%s", s); n = strlen(s);
	scanf("%s", p); m = strlen(p);
	get_nxt();
	kmp();
	for (int i = 1; i <= cnt; i++)printf("%d\n", ans[i] + 1);
	for (int i = 1; i <= m; i++)printf("%d%s", nxt[i], i == m ? "\n" : " ");
}

最小迴圈節

字串 \(p\) 的最小迴圈節的長度為 \(m - nxt[m]\)

4.2 Manacher

\(O(n)\) 複雜度求解最長迴文子串

/*
 * @Author: zhl
 * @LastEditTime: 2020-12-07 10:50:16
 */
class Solution {
public:
    string longestPalindrome(string s) {
        int n = s.length();
        if (n < 2)return s;
        string t = "$";
        for (int i = 0;i < n;i++) {
            t += "#" + s[i];
        }
        t += "#@";
        n = t.length();
        vector<int>p;p.resize(n + 10);

        int id = 0, mx = 0;int maxlen = 0, cen = 0;
        for (int i = 1;i < n - 1;i++) {
            p[i] = i < mx ? min(mx - i, p[2 * id - i]) : 1;
            while (t[i - p[i]] == t[i + p[i]])p[i]++;
            if (i + p[i] > mx) {
                mx = i + p[i];
                id = i;
            }
            if (p[i] - 1 > maxlen) {
                maxlen = p[i] - 1;
                cen = i;
            }
        }

        int st = (cen - maxlen) / 2;
        return s.substr(st, maxlen);
    }
};

4.3 Ac自動機

多模式匹配,根據多模式串建立 Tire 樹, 然後建立 Fail 指標, Fail 指標指的是最長字尾

/*
 * @Author: zhl
 * @Date: 2020-10-14 11:36:01
 */
#include<bits/stdc++.h>
using namespace std;
// 求模式串出現了多少個,所以每個串只能訪問一次
const int N = 6e6 + 10;
queue<int>q;
struct {
	int c[N][26], fail[N], val[N], cnt;
	int newnode() {
		++cnt;
		for (int i = 0; i < 26; i++)c[cnt][i] = 0;
		fail[cnt] = 0;
		return cnt;
	}
	void init() {
		cnt = 0;
		for (int i = 0; i < 26; i++)c[0][i] = 0;
		fail[0] = 0;
	}
	void insert(char* s) {
		int len = strlen(s); int now = 0;
		for (int i = 0; i < len; i++) {
			int v = s[i] - 'a';
			if (!c[now][v])c[now][v] = newnode();
			now = c[now][v];
		}
		val[now]++;
	}
	void getFail() {
		for (int i = 0; i < 26; i++) {
			if (c[0][i])fail[c[0][i]] = 0, q.push(c[0][i]);
		}
		while (!q.empty()) {
			int u = q.front(); q.pop();
			for (int i = 0; i < 26; i++) {
				if (c[u][i]) {
					fail[c[u][i]] = c[fail[u]][i];
					q.push(c[u][i]);
				}
				else c[u][i] = c[fail[u]][i];
			}
		}
	}
	int query(char* s) {
		int len = strlen(s); int now = 0, ans = 0;
		for (int i = 0; i < len; i++) {
			now = c[now][s[i] - 'a'];
			for (int t = now; t && val[t] != -1; t = fail[t]) {
				ans += val[t];
				val[t] = -1;
			}
		}
		return ans;
	}
}Ac;

int n;
char p[N];

int main() {
	scanf("%d", &n);
	Ac.init();
	for (int i = 1; i <= n; i++) {
		scanf("%s", p);
		Ac.insert(p);
	}
	Ac.getFail();
	scanf("%s", p);
	printf("%d\n", Ac.query(p));
}

AC自動機經常和狀態轉移的題目有關,可以做狀態轉移矩陣加矩陣快速冪計數。也可以結合狀態轉移進行高斯消元解方程。

Poj2778

其實Ac自動機的Tire樹就是一個狀態轉移圖,構造出狀態轉移矩陣, \(M_{ij}\) 表示從Tire樹上的第 \(i\) 個節點轉移到 \(j\) 節點的方案數, \(M^n\) 就是長度為 \(n\) 的串的狀態轉移矩陣, \(M_{0i}\) 表示從根節點轉移到 \(i\) 經過 \(n\) 次的方案數,\(ans= \sum_iM_{0i}\)

在處理Tire樹的時候要稍微注意一些小的細節。

主要就是標記的傳遞

if(val[fail[u]]) val[u] = 1

以輸入:

4 3
AT
AC
AG
AA

為例

#include<cstdio>
#include<map>
#include<cstring>
#include<queue>
#include<string>
#define int long long
using namespace std;

const int N = 5e5 + 10;
queue<int>q;
const int mod = 1e5;
map<char, int>id;
struct Mat {
    int m[100][100], n;
    Mat(int _n, int v) {
        n = _n;
        memset(m, 0, sizeof m);
        for (int i = 0; i < n; i++)m[i][i] = v;
    }
    Mat operator *(const Mat& b)const {
        Mat res = Mat(b.n, 0);
        int n = b.n;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                for (int k = 0; k < n; k++) {
                    res.m[i][j] = (res.m[i][j] + m[i][k] * b.m[k][j]) % mod;
                }
            }
        }
        return res;
    }
};
struct {
    int c[N][4], fail[N], val[N], cnt;
    void insert(char* s) {
        int len = strlen(s); int now = 0;
        for (int i = 0; i < len; i++) {
            int v = id[s[i]];
            if (!c[now][v])c[now][v] = ++cnt;
            now = c[now][v];
        }
        val[now]++;//這裡寫++好像過不去
        //val[now] = 1;
    }
    void clear() {
        memset(c, 0, sizeof c);
        memset(val, 0, sizeof val);
        cnt = 0;
        memset(fail, 0, sizeof fail);
    }
    void getFail() {
        for (int i = 0; i < 4; i++) {
            if (c[0][i])fail[c[0][i]] = 0, q.push(c[0][i]);
        }


        while (!q.empty()) {
            int u = q.front(); q.pop();

            //***
            if (val[fail[u]] == 1) {
                val[u] = 1;
            }

            for (int i = 0; i < 4; i++) {
                if (c[u][i]) {
                    fail[c[u][i]] = c[fail[u]][i];
                    q.push(c[u][i]);
                }
                else c[u][i] = c[fail[u]][i];
            }
        }
    }
    int query(char* s) {
        int len = strlen(s); int now = 0, ans = 0;
        for (int i = 0; i < len; i++) {
            now = c[now][id[s[i]]];
            for (int t = now; t && val[t] != -1; t = fail[t]) {
                ans += val[t];
                val[t] = -1;
            }
        }
        return ans;
    }
    Mat getMat() {
        //這裡是cnt + 1
        Mat res = Mat(cnt + 1, 0);
        for (int i = 0; i <= cnt; i++) {
            for (int j = 0; j < 4; j++) {
                if (!val[c[i][j]]) {
                    res.m[i][c[i][j]]++;
                }
            }
        }
        return res;
    }

}Ac;

Mat qpow(Mat a, int p) {
    Mat res = Mat(a.n, 1);
    while (p) {
        if (p & 1) res = a * res;
        a = a * a;
        p >>= 1;
    }
    return res;
}

int n;
char p[N];


signed main() {
    char s[] = "ACGT";
    for (int i = 0; i < 4; i++)id[s[i]] = i;

    int n, m, x;
    while (~scanf("%lld%lld", &m, &n)) {
        Ac.clear();
        for (int i = 0; i < m; i++) {
            scanf("%s", p);
            Ac.insert(p);
        }
        Ac.getFail();
        Mat mat = Ac.getMat();
        mat = qpow(mat, n);

        int ans = 0;
        for (int i = 0; i < mat.n; i++) {
            ans = (ans + mat.m[0][i]) % mod;
        }
        printf("%lld\n", ans);
    }
}

4.4 字尾陣列

字尾陣列可以求出一個串 \(s\) 的所有後綴的排名

有兩種演算法

倍增\(O(nlogn)\) 常數小

DC3\(O(n)\) 常數大

這裡使用倍增就可以

\(O(nlogn)\) 的時間求出以下資訊

sa 陣列, sa[i] 表示排第 i 位的是第 sa[i] 個字尾

rk 陣列, rk[i] 表示第 i 個字尾的排名是 rk[i]

height[i] 表示第 sa[i] 個字尾與 sa[i-1] 的最長公共字首

如何倍增

首先把所有後綴按照第一個字母排序,使用 \(O(n)\) 的基數排序

假設已經按前 \(k\) 個字母排好序,下輪考慮前 \(2k\) 個字母

我們把前 \(k\) 個字母看作第一關鍵字, 後 \(k\) 個字母看作第二關鍵字

則只需要按照第二關鍵字排好序,然後再按第一關鍵字進行穩定的基數排序,就可以完成按照前 \(2k\) 個字母排序

我們發現,第 \(i\) 個字尾的第二關鍵字是第 \(i + k\) 個字尾的第一關鍵字

void get_sa() {
    //先按照第一個字母排序
	for (int i = 1; i <= n; i++) c[x[i] = s[i]] ++;
	for (int i = 2; i <= m; i++) c[i] += c[i - 1]; //小於等於i的數目
    
	for (int i = n; i; i--) sa[c[x[i]] --] = i; 
    
    //開始倍增
	for (int k = 1; k <= n; k <<= 1){
		int num = 0;

		for (int i = n - k + 1; i <= n; i++) y[++num] = i; //第二關鍵字是空串,肯定在最前面
		for (int i = 1; i <= n; i++)
			if (sa[i] > k)
				y[++num] = sa[i] - k;

		for (int i = 1; i <= m; i++) c[i] = 0;
		for (int i = 1; i <= n; i++) c[x[i]] ++;
		for (int i = 2; i <= m; i++) c[i] += c[i - 1];
		
        //按第二關鍵字倒序列舉
		for (int i = n; i; i--) sa[c[x[y[i]]] --] = y[i], y[i] = 0;
		swap(x, y); //把 x 暫時存到 y 中

		//離散化
		x[sa[1]] = 1, num = 1;
		for (int i = 2; i <= n; i++)
			x[sa[i]] = (y[sa[i]] == y[sa[i - 1]] && y[sa[i] + k] == y[sa[i - 1] + k]) ? num : ++num;
		if (num == n) break;
		m = num;
	}
}

如何求 height 陣列

首先定義

\(lcp(i,j)\) 表示排名\(i\) 的字尾和排名\(j\) 的字尾的最長公共字首長度

則顯然有一下幾條性質

  • \(lcp(i,j) = lcp(j,i)\)
  • \(lcp(i,i) = len(i)\)

還有一條如下的性質, 對於 $ i \le k \le j$

\[lcp(i,j) = min\bigg\{lcp(i,k),lcp(k,j)\bigg\} \]

\(i\)\(j\)\(y\) 處的字元不會相等,若相等則 \(lcp(i,k)\) 可以繼續擴充套件

由此可以推出

\[lcp(i,j) = min\bigg\{ lcp(i,i+1),\ lcp(i+1,i+2),\ ...,\ lcp(j-1,j) \bigg\} \]

至此,我們來考慮 height 的求法

\(height(i) = lcp(i-1,i)\)

\(h(i) = height(rk[i])\) , 第 \(i\) 個字尾與排名在它前一位的字尾的 \(lcp\)

我們考慮第 \(i - 1\) 個字尾,設第 \(k\) 個字尾是排名在它前一位的字尾

\[lcp(rk[i-1],rk[k]) = lcp(rk[i],rk[k+1]) + 1 \]

\[lcp(rk[i],rk[k]) = h(i-1)- 1 \]

根據之前推的性質,排名在第 \(i\) 個字尾的前一位的字尾不妨在 \(k\) 之前,

\[h(i) \ge h(i-1) - 1 \]

有了這一條性質後,我們可以在 \(O(n)\) 時間內求出 height 陣列

void get_height()
{
	for (int i = 1; i <= n; i++) rk[sa[i]] = i;
	for (int i = 1, k = 0; i <= n; i++)
	{
		if (rk[i] == 1) continue;
		if (k) k--; //只需要從 h[i-1] - 1 開始列舉就可以
		int j = sa[rk[i] - 1];
		while (i + k <= n && j + k <= n && s[i + k] == s[j + k]) k++;
		height[rk[i]] = k;
	}
}

P3809 【模板】字尾排序 - 洛谷

/*
 * @Author: zhl
 * @Date: 2020-11-23 15:14:44
 */
#include<bits/stdc++.h>
using namespace std;

const int N = 1000010;

int n, m;
char s[N];
int sa[N], x[N], y[N], c[N], rk[N], height[N];
/*
	sa[i] :
	x[i] : 第一關鍵字
	y[i] : 第二關鍵字
	c[i] : 桶
	rk[i] : 
*/

void get_sa(){
	/*
		根據首字母進行基數排序
	*/
	for (int i = 1; i <= n; i++) c[x[i] = s[i]] ++;
	for (int i = 2; i <= m; i++) c[i] += c[i - 1];
	for (int i = n; i; i--) sa[c[x[i]] --] = i;  // s[i] = k 表示 rank i 的串從 k 位置開始

	/*
		開始倍增
	*/
	for (int k = 1; k <= n; k <<= 1)
	{	
		/*
			此時已經根據前k個字母排好序,要根據2*k個字母排好序
			先按照後 k 個字母(第二關鍵字)排序,再根據前 k 個字母排序(穩定排序不會改變相對位置)
		*/
		int num = 0;
		
		for (int i = n - k + 1; i <= n; i++) y[++num] = i; // 這個區間第二關鍵字是 空串
		for (int i = 1; i <= n; i++) //已經按前 k 個字母排序, 第 i 個字尾的第二關鍵字是 第 i + k的第一關鍵字
			if (sa[i] > k)
				y[++num] = sa[i] - k;
		


		for (int i = 1; i <= m; i++) c[i] = 0;
		for (int i = 1; i <= n; i++) c[x[i]] ++;
		for (int i = 2; i <= m; i++) c[i] += c[i - 1];

		// 按照第二關鍵字的順序從後往前列舉
		for (int i = n; i; i--) sa[c[x[y[i]]] --] = y[i], y[i] = 0;
		swap(x, y); //把 x 暫時存到 y 中

		//離散化
		x[sa[1]] = 1, num = 1;
		for (int i = 2; i <= n; i++)
			x[sa[i]] = (y[sa[i]] == y[sa[i - 1]] && y[sa[i] + k] == y[sa[i - 1] + k]) ? num : ++num;
		if (num == n) break;
		m = num;
	}
}

/*
	h[i] = height[rk[i]]
	h[i] >= h[i-1] - 1
	k 是當前的 h[i]
*/
void get_height()
{
	for (int i = 1; i <= n; i++) rk[sa[i]] = i;
	for (int i = 1, k = 0; i <= n; i++)
	{
		if (rk[i] == 1) continue;
		if (k) k--; //只需要從 h[i-1] - 1 開始列舉就可以
		int j = sa[rk[i] - 1];
		while (i + k <= n && j + k <= n && s[i + k] == s[j + k]) k++;
		height[rk[i]] = k;
	}
}

int main()
{
	scanf("%s", s + 1);
	n = strlen(s + 1), m = 122;
	get_sa();
	get_height();

	for (int i = 1; i <= n; i++) printf("%d ", sa[i]);
	puts("");
	for (int i = 1; i <= n; i++) printf("%d ", height[i]);
	puts("");
	return 0;
}

求所有子串

所有的子串就是所有後綴的所有不同字首

$ n + 1 -sa[i]$ 是字尾的長度,所有長度在 \(height[i]\) 以內的字首都被統計過了,所以要減去

int main() {
	scanf("%d", &n);
	scanf("%s", s + 1);
	m = 122;
	get_sa();
	get_height();
	long long ans = 0;
	for (int i = 1; i <= n; i++) {
		ans += n + 1 - sa[i] - height[i];
	}
	printf("%lld\n", ans);
}

4.5 字尾自動機

字尾自動機是一個自動機

原串的所有子串和從SAM起點開始的所有路徑一一對應,不重不漏。所以終點就是包含所有後綴的點。

程式碼中的根節點是 1,上圖中是 0,注意區別

一、\(SAM\) 的性質:

\(SAM\) 是個狀態機。一個起點,若干終點。原串的所有子串和從 \(SAM\) 起點開始的所有路徑一一對應,不重不漏。所以終點就是包含字尾的點。

每個點包含若干子串,每個子串都一一對應一條從起點到該點的路徑。且這些子串一定是裡面最長子串的連續後綴。

\(SAM\) 問題中經常考慮兩種邊:

(1) 普通邊,類似於 \(Trie\)。表示在某個狀態所表示的所有子串的後面新增一個字元。

(2) \(Link、Father\)。表示將某個狀態所表示的最短子串的首字母刪除。這類邊構成一棵樹。

二、\(SAM\) 的構造思路

\(endpos(s)\):子串 \(s\) 所有出現的位置(尾字母下標)集合。\(SAM\) 中的每個狀態都一一對應一個 \(endpos\) 的等價類。

\(endpos\) 的性質:

(1) 令 \(s1,s2\)\(S\) 的兩個子串 ,不妨設 \(|s1|≤|s2|\) (我們用 \(|s|\) 表示 \(s\) 的長度 ,此處等價於 \(s1\) 不長於 \(s2\) )。

\(s1\)\(s2\) 的字尾當且僅當 \(endpos(s1)⊇endpos(s2)\)\(s1\) 不是 \(s2\) 的字尾當且僅當 en\(dpos(s1)∩endpos(s2)=∅\) 。

(2) 兩個不同子串的 \(endpos\),要麼有包含關係,要麼沒有交集。

(3) 兩個子串的 \(endpos\) 相同,那麼短串為長串的字尾。

(4) 對於一個狀態 \(st\) ,以及任意的 \(longest(st)\) 的字尾 s ,如果 \(s\) 的長度滿足:\(|shortest(st)|≤|s|≤|longsest(st)| ,\)那麼 \(s∈substrings(st)\)

演算法:

字尾自動機 (SAM) - OI Wiki

P3804 【模板】字尾自動機 (SAM) - 洛谷

#include<bits/stdc++.h>
using namespace std;

const int N = 2e6 + 10;

struct Edge {
	int to, next;
}E[N];
int head[N], cnt;
void addEdge(int from, int to) {
	E[cnt] = { to,head[from] };
	head[from] = cnt++;
}
int tot = 1, last = 1;
struct Node {
	int len, fa;
	int ch[26];
}node[N];

typedef long long ll;
char s[N];
ll f[N];

void extend(int c) {
	int p = last, np = last = ++tot;
	f[tot] = 1;
	node[np].len = node[p].len + 1;
	for (; p && !node[p].ch[c]; p = node[p].fa) node[p].ch[c] = np;
	if (!p)node[np].fa = 1;
	else {
		int q = node[p].ch[c];
		if (node[q].len == node[p].len + 1) node[np].fa = q;
		else {
			int nq = ++tot;
			node[nq] = node[q], node[nq].len = node[p].len + 1;
			node[q].fa = node[np].fa = nq;
			for (; p and node[p].ch[c] == q; p = node[p].fa) node[p].ch[c] = nq;
		}
	}
}
ll ans;
void dfs(int u) {
	for (int i = head[u]; ~i; i = E[i].next) {
		dfs(E[i].to);
		f[u] += f[E[i].to];
	}
	if (f[u] > 1) ans = max(ans, f[u] * node[u].len);
}
int main() {
	scanf("%s", s);
	for (int i = 0; s[i]; i++)extend(s[i] - 'a');
	memset(head, -1, sizeof head);
	for (int i = 2; i <= tot; i++) {
		addEdge(node[i].fa, i);
	}
	dfs(1);
	printf("%lld\n", ans);
}

H - String and Times

詢問子串出現次數範圍在 \([A,B]\) 內的子串個數

\(SAM\) 很簡單做,毒瘤題目沒有說單組資料多大,就給了個 \(\sum|S| \le 2e6\)

程式碼中的根節點是 1,上圖中是 0,注意區別

圖中的 last 集合就是每次的 last

在求 endpos 的時候按照 len 排序就是拓撲序

然後按拓撲序加就可以

/*
 * @Author: zhl
 * @Date: 2020-11-26 13:30:44
 */
#include<bits/stdc++.h>
using namespace std;

const int N = 4e5 + 10;
int tot = 1, last = 1;
struct Node {
	int len, fa;
	int ch[26];
}tr[N];

char s[N];

int sum[N], tp[N], cnt[N];
void extend(int c) {
	int p = last, np = last = ++tot;
	cnt[last] = 1;
	tr[np].len = tr[p].len + 1;
	for (; p && !tr[p].ch[c]; p = tr[p].fa) tr[p].ch[c] = np;
	if (!p)tr[np].fa = 1;
	else {
		int q = tr[p].ch[c];
		if (tr[q].len == tr[p].len + 1) tr[np].fa = q;
		else {
			int nq = ++tot;
			tr[nq] = tr[q]; tr[nq].len = tr[p].len + 1;
			tr[q].fa = tr[np].fa = nq;
			for (; p and tr[p].ch[c] == q; p = tr[p].fa) tr[p].ch[c] = nq;
		}
	}
}


void topo() {
	for (int i = 1; i <= tr[last].len; i++)sum[i] = 0;
	for (int i = 1; i <= tot; i++) sum[tr[i].len]++;
	for (int i = 1; i <= tr[last].len; i++)sum[i] += sum[i - 1];
	for (int i = 1; i <= tot; i++)tp[sum[tr[i].len]--] = i;
}
void init() {
	last = tot = 1;
	memset(cnt, 0, sizeof cnt);
	memset(tr, 0, sizeof tr);
}

int main() {
	while (~scanf("%s", s)) {
		init();
		int a, b; scanf("%d%d", &a, &b);
		
		for (int i = 0; s[i]; i++) extend(s[i] - 'A');

		topo();
		long long ans = 0;
		for (int i = tot; i >= 1; i--) {
			int p = tp[i], fp = tr[p].fa;
			cnt[fp] += cnt[p];
			if (cnt[p] >= a and cnt[p] <= b) ans += tr[p].len - tr[fp].len;
		}
		printf("%lld\n", ans);
	}
}

求所有子串

每個節點的子串數目是 當前節點的最長長度減去父節點的最長長度。

/*
 * @Author: zhl
 * @Date: 2020-11-24 10:30:44
 */
#include<bits/stdc++.h>
using namespace std;

const int N = 2e6 + 10;
int tot = 1, last = 1;
struct Node {
	int len, fa;
	int ch[26];
}tr[N];

typedef long long ll;
char s[N];
ll f[N];

void extend(int c) {
	int p = last, np = last = ++tot;
	f[tot] = 1;
	tr[np].len = tr[p].len + 1;
	for (; p && !tr[p].ch[c]; p = tr[p].fa) tr[p].ch[c] = np;
	if (!p)tr[np].fa = 1;
	else {
		int q = tr[p].ch[c];
		if (tr[q].len == tr[p].len + 1) tr[np].fa = q;
		else {
			int nq = ++tot;
			tr[nq] = tr[q], tr[nq].len = tr[p].len + 1;
			tr[q].fa = tr[np].fa = nq;
			for (; p and tr[p].ch[c] == q; p = tr[p].fa) tr[p].ch[c] = nq;
		}
	}
}

int main() {
	int n; scanf("%d", &n);
	scanf("%s", s);
	for (int i = 0; s[i]; i++)extend(s[i] - 'a');
	long long ans = 0;
	for (int i = 1; i <= tot; i++) {
		ans += tr[i].len - tr[tr[i].fa].len;
	}
	printf("%lld\n", ans);
}

4.6 迴文自動機

迴文自動機也叫回文樹,可以處理出一個字串的所有迴文子串以及它們的出現次數

/*
 * @Author: zhl
 * @Date: 2020-11-24 11:57:26
 */
#include<bits/stdc++.h>
using namespace std;

const int N = 2e6 + 10;

struct Node {
	int ch[26], fail;
	int len, sum; //這裡的sum不是這個串的出現次數,而是迴文子串的數目
}tr[N];

char s[N];
int last, tot = 2;

int newnode(int len) {
	tr[tot].len = len;
	//tr[tot].fail = 0;
	//for(int i = 0;i < 26;i++)tr[tot].ch[i] = 0;
	return tot++;
}
int getFail(int x, int pos) {
	while (s[pos - tr[x].len - 1] != s[pos]) x = tr[x].fail;
	return x;
}

void init() {
	tr[0].len = 0, tr[1].len = -1;
	tr[0].fail = 1; tr[1].fail = 0;
	//for(int i = 0;i < 26;i++)tr[0].ch[i] = tr[1].ch[i] = 0;
	last = 0;
}

void insert(int pos) {
	int cur = getFail(last, pos);
	int c = s[pos] - 'a';
	if (tr[cur].ch[c] == 0) {
        // 出現了新的本質不同的迴文串
		int now = newnode(tr[cur].len + 2);
		tr[now].fail = tr[getFail(tr[cur].fail, pos)].ch[c]; //fail指向字尾中的最長迴文串
		tr[now].sum = tr[tr[now].fail].sum + 1;
		tr[cur].ch[c] = now;
	}
	last = tr[cur].ch[c];
}

int main() {
	scanf("%s", s + 1);
	int k = 0; int n = strlen(s + 1);
	init();
	for (int i = 1; i <= n; i++) {
		s[i] = (s[i] - 97 + k) % 26 + 97;
		insert(i);
		printf("%d ", tr[last].sum);
		k = tr[last].sum;
	}
}

[P3649 [APIO2014]迴文串 - 洛谷 ](https://www.luogu.com.cn/problem/P3649)

求每個迴文子串的長度乘出現次數的最大值

/*
 * @Author: zhl
 * @Date: 2020-11-24 15:09:25
 */
#include<bits/stdc++.h>
using namespace std;

const int N = 2e6 + 10;

struct Node {
	int ch[26], fail;
	int len, sum;
}tr[N];

char s[N];
int last, tot = 2;

int newnode(int len) {
	tr[tot].len = len;
	//tr[tot].fail = 0;
	//for(int i = 0;i < 26;i++)tr[tot].ch[i] = 0;
	return tot++;
}
int getFail(int x, int pos) {
	while (s[pos - tr[x].len - 1] != s[pos]) x = tr[x].fail;
	return x;
}

void init() {
	tr[0].len = 0, tr[1].len = -1;
	tr[0].fail = 1; tr[1].fail = 0;
	//for(int i = 0;i < 26;i++)tr[0].ch[i] = tr[1].ch[i] = 0;
	last = 0;
}
int cnt[N];
void insert(int pos) {
	int cur = getFail(last, pos);
	int c = s[pos] - 'a';
	if (tr[cur].ch[c] == 0) {
		int now = newnode(tr[cur].len + 2);
		tr[now].fail = tr[getFail(tr[cur].fail, pos)].ch[c]; //fail指向字尾中的最長迴文串
		tr[now].sum = tr[tr[now].fail].sum + 1;
		tr[cur].ch[c] = now;
	}
	last = tr[cur].ch[c];
	cnt[last]++;
}


int main() {
	scanf("%s", s + 1);
	int n = strlen(s + 1);
	init();
	for (int i = 1; i <= n; i++) {
		insert(i);
	}
	long long ans = 0;
	for (int i = tot; i; i--) { //倒過來其實拓撲序
		cnt[tr[i].fail] += cnt[i];
		ans = max(ans, 1ll * cnt[i] * tr[i].len);
	}
	printf("%lld\n", ans);
}

4.7 最小表示法

與字串 \(S\) 迴圈同構的字典序最小串

#include<bits/stdc++.h>
using namespace std;

const int N = 3e5 + 10;
int A[N], n;
int mp(int* sec,int n) {
	int k = 0, i = 0, j = 1;
	while (k < n && i < n && j < n) {
		if (sec[(i + k) % n] == sec[(j + k) % n]) {
			k++;
		}
		else {
			sec[(i + k) % n] > sec[(j + k) % n] ? i = i + k + 1 : j = j + k + 1;
			if (i == j) i++;
			k = 0;
		}
	}
	return min(i, j);
}

int main() {
	scanf("%d", &n);
	for (int i = 0; i < n; i++)scanf("%d", A + i);
	int pos = mp(A,n);
	for (int i = pos; i < n; i++)printf("%d ", A[i]);
	for (int i = 0; i < pos; i++)printf("%d ", A[i]);
}

五、數學

5.0 數論函式

積性函式

如果已知一個函式為數論函式,且\(f(1)=1\),並且滿足以下條件,若對於任意的兩個互質的正整數\(p,q\)都滿足\(f(p⋅q)=f(p)⋅f(q)\),那麼則稱這個函式為積性函式

常見的積性函式

\(1. \mu(n)\)

莫比烏斯函式

  • $\mu(1) = 1 $

  • \(d=\Pi_{i=1}^{k}p_i\), 且 \(p_i\) 為互異素數時,\(\mu(d) = (-1)^k\)

  • \(d\) 含有任何質因子的冪次大於等於 \(2\) ,則 \(\mu(d) = 0\)

\(\mu * I = \epsilon\)

$2. \varphi(n) $

尤拉函式

表示 \([1,n)\) 內與 \(n\) 互質的數的個數

\(\varphi * I = id\)

\(\rightarrow \varphi * I * \mu = id * \mu\)

\(\rightarrow \varphi = id * \mu=\sum_{d|n}\mu(d)\cdot \dfrac n d\)

\[\frac{\varphi(n)}{n}=\sum_{d|n}\frac{\mu(d)}{d} \]

\(3. d(n)\)

約數個數

\[d(ij) = \sum_{x|i}\sum_{y|j}[gcd(x,y)=1] \]

證明:

\[d(n) =\prod_{i=1}^n(1+a_i),\ n = \prod_{i=1}^np_i^{a_i} \]

\(4. \sigma(n)\)

約數和函式

完全積性函式

\(1. \epsilon(n)\)

元函式, \(\epsilon(n) = [n==1]\)

\(f * \epsilon = f\)

\(2. I(n)\)

恆等函式, \(I(n) = 1\)

\(I * f = \sum_{d|n}f(d)\)

\(3. id(n)\)

單位函式, \(id(n) = n\)

\(lcy\)\(ppt\)

尤拉函式

\[\varphi(n) = \begin{cases} 1 ,\ \ \ \ \ \ \ \ \ \ \ \ \text{n = 1} \\ numbers(gcd(n,i) == 1 \ for\ i\ \ in\ \ [1,n]) \end{cases}\]

尤拉函式\(\varphi(n)\)等於小於等於n的所有正整數中和n互素的數的個數

一些性質

性質一:

對於素數\(p\)

\[\varphi(p) = p - 1 \]

很顯然,除了\(p\) 1到\(p-1\)和都\(p\)互素

性質二:

對於素數\(p\)

\[\varphi(p^k) = p^k - p^{k-1}=p^k(1-\frac1p) \]

證明:
對於[1,\(p^k\)]中的數n,若與\(p^k\)不互素,則必有

\[n = xp \]

\(x\in[1,2,...,p^{k-1}]\)
所以\(\varphi(p^k) = p^k - p^{k-1}\)得證,第一個式子是總數,第二是不互素的數的個數

性質三:(積性)

對於素數\(p\),\(q\)

\[\varphi(p*q) = \varphi(p) \varphi(q) \]

性質四: (計算公式)

對於任意正整數\(n\) 其中 \(n = \prod_{i=1}^{i = k}p_i^{e_i}\)

\[\varphi(n) = n\prod_{i=1}^{i = k}(1-\frac1{p_i}) \]

計算程式碼

int euler_phi(int n) {
    int m = (int)sqrt(n + 0.5);
    int ans = n;
    for (int i = 2; i <= m; ++i) {
        if (n % i == 0) {
            ans = ans / i *(i - 1);
            while (n % i == 0) n /= i;
        }
    }
    if (n > 1) ans = ans / n *(n - 1);
    return ans;
}

所以在尤拉篩中

if (i % prime[j] == 0) {//因數都一樣,多乘了一個p
	phi[i * prime[j]] = phi[i] * prime[j];
	break;
}
else {//積性
	phi[i * phi[j]] = phi[i] * (phi[j] - 1);
}

證明:

\[\varphi(n) = \varphi(\prod_{i=1}^{i = k}p_i^{e_i}) \]

\[= \prod_{i=1}^{i = k}\varphi(p_i^{e_i}) \]

\[= \prod_{i=1}^{i = k}(p_i^{e_i}(1-\frac1{p_i})) \]

\[= n\prod_{i=1}^{i = k}(1-\frac1{p_i}) \]

證畢

性質五:

\(i \ mod \ p = 0\)

\[\varphi(i*p)=p*\varphi(i) \]

\(i \ mod \ p \neq 0\)

\[\varphi(i*p)=(p-1)*\varphi(i) \]

這條性質顯然,由積性直接得出(\(i\),\(p\)互質)

對於第一條性質,首先要知道一個引理

\[if \ gcd(n,i) = 1 \ then \ gcd(n + i,i) = 1 \]

證明
\(n+i\)\(i\)有公因數\(k\),則\(n\)\(i\)也有公因數\(k\)矛盾
由此
\(gcd(a,b)=1\),則\(gcd(a,b+ka)=1\)\(k\in Z\)(\(k\)可以是負數)

因為\(i \ mod \ p = 0\),所以對於\(k\in[1,i]\)\(gcd(k,i)=1\)\(gcd(k,i*p)\)是等價的
\(gcd(k+xi,i)=1\)\(x\in[0,1,2,..,p-1]\)
對於\(k>i\)\(k\)來說,一定是由\(k\in[1,i]\)轉移而來

所以 \(\varphi(i*p)=p*\varphi(i)\)

尤拉定理:

\(gcd(a,n) == 1\) ,則

\[a^{\varphi(n)} \ mod\ n \ = 1 \]

廣義尤拉定理

\[a^b(mod\ n) = \begin{cases} a^{b\ \% \ \varphi(n)} &(mod\ n),&&a和n互質\\ a^b \ & \ (mod \ n),&&b<\varphi(n)\\ a^{b\ \% \ \varphi(n)+\varphi(n)}&(mod\ n),&&b\ge\varphi(n) \end{cases} \]

一般用作降冪,也可以用來求逆元

\(\sum_{d|n} \varphi(d) = n\)

\[\sum_{d|n}\varphi(d) = n \]

證明

對於 \(n = 1\) ,不難驗證滿足題意

對於 \(n = p^a\) ,

\[\sum_{d|n}\varphi(d) = 1 + \sum_{i = 1}^a\varphi(p^i) \\=p^a = n \]

對於 \(n = p_1^{a_1}...p_k^{a_k}\)

\[\sum_{d|n}\varphi(d) = \sum_{i=0}^{a_1}\varphi(p_1^i)\sum_{i=0}^{a_2}\varphi(p_2^i)...\sum_{i=0}^{a_k}\varphi(p_k^i)\\=p_1^{a_1}...p_k^{a_k} = n \]

莫比烏斯函式

\[\sum_{d|n}\varphi(d) = \sum_{i=0}^{a_1}\varphi(p_1^i)\sum_{i=0}^{a_2}\varphi(p_2^i)...\sum_{i=0}^{a_k}\varphi(p_k^i)\\=p_1^{a_1}...p_k^{a_k} = n \]

先介紹莫比烏斯函式 \(\mu\) ,是一個常用的積性函式

\[\mu(x)= \begin{cases} 1,& \text{x = 1}\\ 0,& \text{x的任何因子冪次數大於二}\\ (-1)^{k}, & x = \prod_{i=1}^kp_i,p_i為互異的素數 \end{cases} \]

一、性質

①、常用

\[\sum_{d|n}\mu(d) = [n=1] \]

其中\([n=1]\)表示n == 1時候成立,為1,否則為0

也就是說任何大於1的整數n,其所有因子的莫比烏斯函式之和為0

②、

對於任意正整數n,

\[\sum_{d|n}\frac{\mu(d)}{d} = \frac{\varphi(n)}{n} \]

這個性質把莫比烏斯函式和尤拉函式聯絡在了一起

莫比烏斯反演

定理:
\(F(n)\)\(f(n)\) 是定義在非負整數集合的兩個函式,且滿足

\[F(n) = \sum_{d|n}f(d) \]

則有

\[f(n) = \sum_{d|n}\mu(d)F(\frac{n}{d})=\sum_{d|n}\mu(\frac{n}{d})F(d) \]

利用卷積的性質不難整明

以及

\[\sum_{d|n}\mu(d) = \sum_{i=0}^k(-1)^iC_k^i \]

\[n = \prod_{i=1}^kp_i^{a_i} \]

5.1 篩法

線性篩

\(O(n)\) 內篩積性函式與素數

void init() {
	mu[1] = phi[1] = 1;
	for (int i = 2; i < N; i++) {
		if (!vis[i])prime[++cnt] = i, mu[i] = -1, phi[i] = i - 1;
		for (int j = 1; j <= cnt and prime[j] * i < N; j++) {
			vis[prime[j] * i] = 1;
			mu[prime[j] * i] = -mu[i];
			phi[prime[j] * i] = (prime[j] - 1) * phi[i];
			if (i % prime[j] == 0) {
				mu[prime[j] * i] = 0;
				phi[prime[j] * i] = phi[i] * prime[j];
				break;
			}
		}
	}
	for (int i = 1; i < N; i++) sm[i] = sm[i - 1] + mu[i], sp[i] = sp[i - 1] + phi[i];
}

杜教篩

\[h = f * g \]

\(S(n) = \sum_{i=1}^{n}f(i)\)

\[\sum_{i=1}^{n}h(i)=\sum_{i=1}^{n}\sum_{d|i}g(d)\cdot f(\frac{i}{d})\\\to =\sum_{d=1}^{n}g(d)\cdot\sum_{i=1}^{\lfloor\frac{n}{d}\rfloor}f({i}) \]

\[\to \sum_{i=1}^{n}h(i)=\sum_{d=1}^{n}g(d)\cdot S(\lfloor\frac{n}{d}\rfloor) \]

\[\sum_{i=1}^{n}h(i)=g(1)\cdot S(n)+\sum_{d=2}^{n}g(d)\cdot S(\lfloor\frac{n}{d}\rfloor) \]

\[\to g(1)S(n)=\sum_{i=1}^{n}h(i)-\sum_{d=2}^{n}g(d)\cdot S(\lfloor\frac{n}{d}\rfloor) \]

洛谷模板,篩 \(\mu\)\(\varphi\) 的字首和

\(\sum_{d|n}\varphi(d) = n\)

\(\sum_{d|n}\mu(d) = [n==1] = \epsilon\)

#include<bits/stdc++.h>
using namespace std;

const int N = 3e6 + 10;
typedef long long ll;

ll prime[N], cnt, mu[N], phi[N], sm[N], sp[N];
bool vis[N];

void init() {
	mu[1] = phi[1] = 1;
	for (int i = 2; i < N; i++) {
		if (!vis[i])prime[++cnt] = i, mu[i] = -1, phi[i] = i - 1;
		for (int j = 1; j <= cnt and prime[j] * i < N; j++) {
			vis[prime[j] * i] = 1;
			mu[prime[j] * i] = -mu[i];
			phi[prime[j] * i] = (prime[j] - 1) * phi[i];
			if (i % prime[j] == 0) {
				mu[prime[j] * i] = 0;
				phi[prime[j] * i] = phi[i] * prime[j];
				break;
			}
		}
	}
	for (int i = 1; i < N; i++) sm[i] = sm[i - 1] + mu[i], sp[i] = sp[i - 1] + phi[i];
}

unordered_map<ll, ll>M, P;
ll m(ll n) {
	if (n < N)return sm[n];
	if (M.count(n)) return M[n];
	ll ans = 1;
	for (ll l = 2, r; l <= n; l = r + 1) {
		r = n / (n / l);
		ans -= (r - l + 1) * m(n / l);
	}
	return M[n] = ans;
}

ll p(ll n) {
	if (n < N)return sp[n];
	if (P.count(n)) return P[n];
	ll ans = n * (n + 1) / 2;
	for (ll l = 2, r; l <= n; l = r + 1) {
		r = n / (n / l);
		ans -= (r - l + 1) * p(n / l);
	}
	return P[n] = ans;
}

ll T, n;
int main() {
	init();
	scanf("%lld", &T);
	while (T--) {
		scanf("%lld", &n);
		printf("%lld %lld\n", p(n), m(n));
	}
}

Min_25

思想:

\[\sum_{i=1}^nf(i) = \sum_{i \in Prime}f(i) + \sum_{i \notin Prime}f(i) \]




分為兩個部分,第一部分是所有素數,第二部分是所有的合數

第一部分

搞來一個這樣的函式 \(g(n,j)\)

\[g(n,j) = \sum_{i=1}^n[i \in Prime\ or\ minp(i) > P_j] i^k \]

所有的素數加上滿足\(minp(i) > P_j\) 的所有 \(i\)

\([1-n]\) 中所有質數的 \(k\) 次方之和就是 \(g(n,x)\)\(P_x\) 是最後一個小於等於

\(\sqrt n\) 的質數

考慮 \(g(n,j)\) 的轉移

\[g(n,j) = g(n,j-1) - P_j^k\bigg(g(\dfrac n {P_j},j-1)\ -g(P_j-1,j-1)\bigg) \]

這個東西自己在紙上寫一些體會一下,注意 \(P_j\) 篩去的第一個數是 \(P_j^2\) , 第二個數不是 \(P_j^2+ P_j\)

第二部分

\[S(n,x) = \sum_{i=1}^n[minp(i) > P_x]f(i) \]

可以把 \(S(n,x)\) 也分成兩部分,一部分是所有大於 \(P_x\) 的質數,另一部分是最小質因數大於 \(P_x\) 的合數,列舉最小質因子

\[S(n,x) = g(n) - sp_x + \sum_{p_k^e \le n \&k>n}f(p_k^e)\bigg(S \bigg(\dfrac n {p_k^e}\bigg) + [e \ne 1]\bigg) \]

\(e = 1\) 的時候, \(P_k\) 在前面列舉過了,不等於 \(1\) 時,需要加上 \(P_k^e\)

存下所有可能的 \(\lfloor\dfrac n x \rfloor\) , 做一個對映

\[idx(x)= \begin{cases}ind1[x] , \ \ x\le \sqrt n \\ind2[n/x],\ \ x>\sqrt n\end{cases} \]

min25模板

P5325 【模板】Min_25篩

積性函式 \(f\)\(f(p^k) = p^k(p^k-1)\)

\(\sum_{i=1}^nf(i)\)

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll mod = 1e9 + 7, inv6 = 166666668, inv2 = 500000004;
const int maxn = 1e6 + 10;
ll n, sqr;

ll prime[maxn], cnt, vis[maxn];
ll sp1[maxn], sp2[maxn];//sp1 p的字首和,sp2 p^2的字首和
ll w[maxn], tot;
ll g1[maxn], g2[maxn], ind1[maxn], ind2[maxn];

void get(int maxn) {
	for (int i = 2; i <= maxn; i++) {
		if (!vis[i]) {
			prime[++cnt] = i;
			sp1[cnt] = (sp1[cnt - 1] + i) % mod;
			sp2[cnt] = (sp2[cnt - 1] + 1ll * i * i) % mod;
		}
		for (int j = 1; j <= cnt && prime[j] * i <= maxn; j++) {
			vis[prime[j] * i] = 1;
			if (i % prime[j] == 0)break;
		}
	}
}
ll S(ll x, int y){
	if (prime[y] >= x)return 0;
	ll k = x <= sqr ? ind1[x] : ind2[n / x];
	ll ans = (g2[k] - g1[k] + mod - (sp2[y] - sp1[y]) + mod) % mod;
	for (int i = y + 1; i <= cnt && prime[i] * prime[i] <= x; i++)
	{
		ll pe = prime[i];
		for (int e = 1; pe <= x; e++, pe = pe * prime[i])
		{
			ll xx = pe % mod;
			ans = (ans + xx * (xx - 1) % mod * (S(x / pe, i) + (e != 1))) % mod;
		}
	}
	return ans % mod;
}

int main() {
	scanf("%lld", &n);
	sqr = sqrt(n);
	get(sqr);
	for (ll l = 1, r; l <= n; l = r + 1) {
		r = n / (n / l);
		w[++tot] = n / l;
		ll k = w[tot] % mod;
		g1[tot] = (k * (k + 1) % mod * inv2 - 1 + mod) % mod;
		g2[tot] = (k * (k + 1) % mod * (2 * k + 1) %mod * inv6 % mod + mod - 1) % mod;


		if (w[tot] <= sqr)ind1[n / l] = tot;
		else ind2[n / (n / l)] = tot;
	}
	for (int i = 1; i <= cnt; i++) {
		//g(n,j) 滾第一維
		for (int j = 1; j <= tot && prime[i] * prime[i] <= w[j]; j++) {
			ll k = w[j] / prime[i] <= sqr ? ind1[w[j] / prime[i]] : ind2[n / (w[j] / prime[i])];
			g1[j] -= prime[i] * (g1[k] - sp1[i - 1] + mod) % mod;
			g2[j] -= prime[i] * prime[i] % mod * (g2[k] - sp2[i - 1] + mod) % mod;
			g1[j] %= mod; g2[j] %= mod;
			if (g1[j] < 0)g1[j] += mod;
			if (g2[j] < 0)g2[j] += mod;
		}
	}
	printf("%lld\n", (S(n, 0) + 1) % mod);
}

5.2 擴充套件歐幾里得

歐幾里得演算法

int gcd(int a,int b){
	return b == 0 ? a : gcd(b, a % b);
}

時間複雜度是 \(O(logn)\)

擴充套件歐幾里得

\[ax + by = c \]

\(g = gcd(a,b)\) , 有 \(g\ \ | \ \ (ax + by)\) , 若 \(g\) 不整除 \(c\) ,則方程沒有整數解

否則,可以用 Exgcd 求解出

\[ax + by = g \]

的一組特解 \((x_0,y_0)\)

而原方程對應的一組特解

\[\begin{cases}x_1 = \dfrac {x_0c} g \\ \\ y_1 = \dfrac {y_0c}g \end{cases} \]

原方程的通解為

\[\begin{cases}x = x_1 + \dfrac {sb} g \\ \\ y = y_1 - \dfrac {sa}g \end{cases} \]

int exgcd(int a, int b, int& x, int& y) {
    //ax + by = gcd(a,b) 求解一組特解 x , y
    if (!b) {
        x = 1;
        y = 0;
        return a;
    }
    int d = exgcd(b, a % b, x, y);
    int t = x;
    x = y;
    y = t - (a / b) * y;
    return d;
}

5.3 擴充套件中國剩餘定理

中國剩餘定理

問題

$x\equiv a_1 ( mod\ m_1) $

\(x \equiv a_2(mod\ m_2)\)

\(......\)

\(x \equiv a_k(mod\ m_k)\)

其中 \(m\) 兩兩互素

int CRT(){
	int res = 0,M = 1;
	int x,y,gcd;

	for(int i = 1;i <= k;i++){
		M *= m[i];
	}

	for(int i = 1;i <= k;i++){
		int tmp = M / m[i];
		ex_gcd(tp,m[i],gcd,x,y);
		x = (x % m[i] + m[i]) % m[i];
		res = (res + tmp * a[i] * x) % M;
	}

	return (res + M) % M;
}

擴充套件中國剩餘定理

取消了兩兩互素的限制

/*
 * @Author: zhl
 * @LastEditTime: 2020-11-30 18:32:04
 */
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N = 1e5 + 10;
typedef long long ll;
ll A[N], m[N];
/*
x = A1 % m1
x = A2 % m2
......
x = An % mn

求解 x 
*/

void ex_gcd(int a, int b, int& gcd, int& x, int& y) {
	if (b == 0) {
		x = 1;
		y = 0;
		gcd = a;
	}
	else {
		ex_gcd(b, a % b, gcd, y, x); //a,b交換,x,y跟著交換,大概應該是這個意思
		y -= x * (a / b);
	}
}
ll mul(ll a, ll b, ll p) {
	ll ans = 0;
	while (b) {
		if (b & 1)ans = (ans + a) % p;
		a = (a + a) % p;
		b >>= 1;
	}
	if (ans < 0)ans += p;
	return ans;
}
int n;
ll EX_CRT() {
	int x, y, gcd;
	int M = m[1]; int res = A[1];

	for (int i = 2; i <= n; i++) {
		int a = M, b = m[i], c = ((A[i] - res) % b + b) % b;
		ex_gcd(a, b, gcd, x, y);
		int tmp = b / gcd;
		if (c % gcd != 0) return -1; //方程無解

		x = mul(x, c / gcd, tmp); //因為係數不為1
		res += x * M;
		M *= tmp;
		res = (res % M + M) % M;
	}
	return (res % M + M) % M;
}

signed main() {
	scanf("%lld", &n);
	for (int i = 1; i <= n; i++) {
		scanf("%lld%lld", m + i, A + i);
	}
	printf("%lld\n", EX_CRT());
}

5.4 莫比烏斯反演

兩種形式

\[F(n) = \sum_{d|n}f(d) \to f(n)=\sum_{d|n}\mu(d)F(\dfrac nd) \]

\[F(n)=\sum_{n|d}f(d) \to f(n) = \sum_{n|d}\mu(\dfrac dn)F(d) \]

基本上就是圍繞

\[\sum_{d|n}\mu(d) = [n==1] \]

\[\sum_{d|n}\varphi(d) = n \]

進行展開

例1

\[\sum_{i=1}^n\sum_{j=1}^m[gcd(i,j) == 1] \ \ \ \ \ \ \ (n < m) \]

直接替換

\[\sum_{i=1}^n\sum_{j=1}^m\sum_{d|gcd(i,j)}\mu(d) \]

列舉 \(d\) ,這裡比較套路

\[\sum_{d=1}^n\mu(d)*\lfloor \dfrac n d \rfloor * \lfloor \dfrac m d \rfloor \]

可以 \(O(\sqrt n)\) 處理了

例2

\[\sum_{i=1}^n\sum_{j=1}^m[gcd(i,j) == k] \]

與上面那個一樣

\[\sum_{i=1}^{\lfloor\dfrac n k \rfloor}\sum_{j=1}^{\lfloor \dfrac m k \rfloor}[gcd(i,j) == 1] \]

例3

\[\sum_{i=1}^n\sum_{j=1}^mgcd(i,j) \]

\[\sum_{i=1}^n\sum_{j=1}^m\sum_{d|gcd(i,j)}\varphi(d) \]

\[= \sum_{d = 1}^n\varphi(d)*\lfloor \dfrac n d \rfloor * \lfloor \dfrac m d \rfloor \]

5.5 生成函式

生成函式

在數學中,某個序列\((a_n)_{n∈N}\) 的母函式(又稱生成函式,英語:\(Generating\ function\))是一種形式冪級數,其每一項的係數可以提供關於這個序列的資訊。

普通生成函式

有三種物品,分別有 3 ,2, 3個,問拿四個的方案數

f[i][j] 表示當前第 i 個位置,已經選了 j 個物品的方案數

f[0][0] = 1;
for(int i = 1;i <= 3;i++){
	for(int j = 0;j <= 8;j++){//總共要選j個
		for(int k = 0;k <= j;k++){//已經選了k個
			if(j - k <= v[i])//此時要選j-k個
				f[i][j] += f[i-1][k];
			
		}
	}
}

第一種物品的生成函式 \(G_1(x) = 1 + x + x^2 + x ^ 3\)

\(G_2(x) = 1 + x + x^2\) , $G_3 = 1 + x + x^2 + x^3 $

\(G_1(x)*G_2(x)*G_3(x)\) ,中 \(x^4\) 的係數就是答案

上述程式碼其實就是在求多項式乘法的係數

指數生成函式

將上述問題改成排列方案hdu1521

構造出

\(G_1(x) = 1+\frac{x^1}{1} + \frac{x^2}{2!} + \frac{x^3}{3!}\)

\(G_2(x) = 1 + \frac{x^1}{1} + \frac{x^2}{2}\)

\(G_3(x) = 1 + \frac{x^1}{1} + \frac{x^2}{2!} + \frac{x^3}{3!}\)

\[\begin{aligned}G_e(x) &= (1 + \frac{x}{1!} + \frac{x^2}{2!} + \frac{x^3}{3!})(1+\frac{x}{1!} + \frac{x^2}{2!}) (1 + \frac{x}{1!} + \frac{x^2}{2!} + \frac{x^3}{3!})\\ &= (1+2x+2x^2+\frac{7}{6}x^3 + \frac{5}{12}x^4 + \frac{1}{12}x^5) (1+x+\frac{1}{2}x^2 + \frac{1}{6}x^3)\\ &=(1+3x + \frac{9}{2}x^2 + \frac{14}{3}x^3 + \frac{35}{12}x^4 + \frac{17}{12}x^5 + \frac{35}{72} x^6 + \frac{8}{72}x^7 + \frac{1}{71}x^8)\end{aligned} \]

答案就是 \(x^4\) 的係數乘上 \(4!\)\(\frac{35}{12} * 4! = 70\)

(1-x)^-1 型

\[\dfrac 1 {1-x} = \sum_{i=0}^\infty x^i \]

廣義二項式定理

\[\dfrac 1 {(1-x)^n} = \sum_{i=0}^{\infty} C_{n+i-1}^i x^i \]

【P2000】拯救世界

至多為 \(k\) 就是 \(\dfrac {1-x^{k+1}} {1-x}\) , 就是 \(1 + x + x^2 + x^3 + ... + x^k\)

\(k\) 的倍數就是 \(\dfrac 1 {1-x^k}\) , 就是

\(1 + x^k + x^{2k} + ...\)

最後的結果是 \(\dfrac 1 {(1-x)^5}\) , 帶入廣義二項式定理, 答案是 \(C_n^4\)

\(py\) 草不過去, \(OI\) 爺直呼 人生苦短, \(ruby\) 用我

e^x 型

\[e^x = \sum_{i=0}^\infty \dfrac {x^i} {i!} \]

5.6 矩陣

主要是一些矩陣模擬題目,還有矩陣快速冪加速數列計算。

struct mat {
	static const int _N = 100;
	int n;
	int m[_N][_N];
	// ll m[_N][_N];
	mat() {}
	mat(int _n, int _v = 1) {
		n = _n;
		memset(m, 0, sizeof m);
		if (_v == 1)for (int i = 0; i < _n; i++)m[i][i] = 1;
	}
	mat(vector<vector<int>>A, int _n) {
		n = _n;
		for (int i = 0; i < n; i++)for (int j = 0; j < n; j++)m[i][j] = A[i][j];
	}
	void input() {
		for (int i = 0; i < n; i++)for (int j = 0; j < n; j++)scanf("%d", &m[i][j]);
	}
	void print() {
		for (int i = 0; i < n; i++)for (int j = 0; j < n; j++)printf("%d%c", m[i][j], " \n"[j == n - 1]);
	}
	mat operator *(const mat& b)const {
		mat res(n, 0);

		for (int i = 0; i < n; i++) {
			for (int j = 0; j < n; j++) {
				for (int k = 0; k < n; k++) {
					//這裡會爆 int 
					res.m[i][j] = (res.m[i][j] + 1ll * m[i][k] * b.m[k][j] % mod) % mod;
				}
			}
		}
		return res;
	}

	mat pow(ll p) {
		mat a = *this, res = mat(n);
		while (p) {
			if (p & 1)res = res * a;
			a = a * a;
			p >>= 1;
		}
		return res;
	}

};

5.7 快速傅立葉變換

FFT 可以用來加速多項式乘法, 時間複雜度 \(O(nlogn)\)

FFT 快速傅立葉變換

\(O(nlogn)\) 計算多項式乘法

參考部落格

係數表示法 轉換為 點值表示法

\[\omega_n^k = cos(\dfrac {2\pi\cdot k} n) + i \cdot sin(\dfrac {2\pi \cdot k} n) \]

\[A(x)=a_0+a_1*x+a_2*{x^2}+a_3*{x^3}+a_4*{x^4}+a_5*{x^5}+\\ \dots+a_{n-2}*x^{n-2}+a_{n-1}*x^{n-1} \]

\[A(x)=(a_0+a_2*{x^2}+a_4*{x^4}+\dots+a_{n-2}*x^{n-2})+\\(a_1*x+a_3*{x^3}+a_5*{x^5}+ \dots+a_{n-1}*x^{n-1}) \]

\[A_1(x)=a_0+a_2*{x}+a_4*{x^2}+\dots+a_{n-2}*x^{\frac{n}{2}-1} \]

\[A_2(x)=a_1+a_3*{x}+a_5*{x^2}+ \dots+a_{n-1}*x^{\frac{n}{2}-1} \]

\[A(x)=A_1(x^2)+xA_2(x^2) \]

帶入 \(x = \omega_n^k\)

\[A(\omega_n^k) = A_1(\omega_{\frac n2}^k) + \omega_n^kA_2(\omega_{\frac n2}^k) \]

帶入 $x = \omega_n^{k+\frac n2} $

\[A(\omega_n^{k+\frac n2}) = A_1(\omega_{\frac n2}^k) -\omega_n^kA_2(\omega_{\frac n2}^k) \]

也就是說如果知道了 $A_1(x),A_2(x) $ 分別在 \(\omega_{\frac n2}^0\) , \(\omega_{\frac n2}^1\) , \(\omega_{\frac n2}^2\) ,...,\(\omega_{\frac n2}^{\frac n2 -1}\) 的取值,

就可以 \(O(n)\) 的求出 \(A(x)\)

void fft(cp *a,int n,int inv)//inv是取共軛複數的符號
{
    if (n==1)return;
    int mid=n/2;
    static cp b[MAXN];
    for(int i = 0;i < mid;i++)b[i]=a[i*2],b[i+mid]=a[i*2+1];
    
    for(int i = 0;i < n;i++)a[i]=b[i];
    fft(a,mid,inv),fft(a+mid,mid,inv);//分治
    
    for(int i = 0;i < mid;i++)
    {
        cp x(cos(2*pi*i/n),inv*sin(2*pi*i/n));//inv取決是否取共軛複數
        b[i]=a[i]+x*a[i+mid],b[i+mid]=a[i]-x*a[i+mid];
    }
    for(int i = 0;i < a;i++)a[i]=b[i];
}

每個位置分治後最終的位置是二進位制翻轉後的位置

void fft(cp *a,int n,int inv)
{
    int bit=0;
    while ((1<<bit)<n)bit++;
    fo(i,0,n-1)
    {
        rev[i]=(rev[i>>1]>>1)|((i&1)<<(bit-1));
        if (i<rev[i])swap(a[i],a[rev[i]]);//不加這條if會交換兩次(就是沒交換)
    }
    for (int mid=1;mid<n;mid*=2)//mid是準備合併序列的長度的二分之一
    {
    	cp temp(cos(pi/mid),inv*sin(pi/mid));//單位根,pi的係數2已經約掉了
        for (int i=0;i<n;i+=mid*2)//mid*2是準備合併序列的長度,i是合併到了哪一位
		{
            cp omega(1,0);
            for (int j=0;j<mid;j++,omega*=temp)//只掃左半部分,得到右半部分的答案
            {
                cp x=a[i+j],y=omega*a[i+j+mid];
                a[i+j]=x+y,a[i+j+mid]=x-y;//這個就是蝴蝶變換什麼的
            }
        }
    }
}

洛谷模板

注意 lim

#include<bits/stdc++.h>
using namespace std;

const double pi = acos(-1.0);
const int N = 3e6 + 10;

struct cp {
	double x, y;
	cp() {}
	cp(double _x, double _y) {
		x = _x; y = _y;
	}
	cp operator + (cp b) {
		return cp(x + b.x, y + b.y);
	}
	cp operator -(cp b) {
		return cp(x - b.x, y - b.y);
	}
	cp operator *(cp b) {
		return cp(x * b.x - y * b.y, x * b.y + y * b.x);
	}
};
int rev[N];
int bit = 0;
int lim;
void FFT(cp* a, int inv) {
	
	for (int i = 0; i < lim; i++) {
		if (i < rev[i]) {
			swap(a[i], a[rev[i]]);
		}
	}
	
	for (int mid = 1; mid < lim; mid <<= 1) {
		cp temp(cos(pi / mid), inv * sin(pi / mid));
		for (int i = 0; i < lim; i += mid * 2) {
			cp omega(1, 0);
			for (int j = 0; j < mid; j++, omega = omega * temp) {
				cp x = a[i + j], y = omega * a[i + j + mid];
				a[i + j] = x + y, a[i + j + mid] = x - y;
			}
		}
	}
}

int n, m;

cp A[N], B[N];

int main() {
	scanf("%d%d", &n, &m);
	
	lim = 1;
	while (lim <= n + m)lim<<=1,bit++;//調整至 2^k

	for (int i = 0; i < lim; i++) {
		rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (bit - 1));
	}
	for (int i = 0; i <= n; i++)scanf("%lf", &A[i].x), A[i].y = 0;
	for (int i = 0; i <= m; i++)scanf("%lf", &B[i].x), B[i].y = 0;

	FFT(A, 1);
	FFT(B, 1);
	for (int i = 0; i <= lim; i++) {
		A[i] = A[i] * B[i];
	}
	FFT(A, -1);
	for (int i = 0; i <= n + m; i++) {
		printf("%d ", int(A[i].x /lim+0.5));
	}

}

NTT

參考部落格

原根

還沒有整太明白

待補,丟一個板子

#include<bits/stdc++.h>
#define swap(a,b) (a^=b,b^=a,a^=b)
using namespace std;

#define LL long long 
const int MAXN = 3 * 1e6 + 10, P = 998244353, G = 3, Gi = 332748118;
char buf[1 << 21], * p1 = buf, * p2 = buf;

int N, M, limit = 1, L, r[MAXN];
LL a[MAXN], b[MAXN];
inline LL fastpow(LL a, LL k) {
	LL base = 1;
	while (k) {
		if (k & 1) base = (base * a) % P;
		a = (a * a) % P;
		k >>= 1;
	}
	return base % P;
}
inline void NTT(LL* A, int type) {
	for (int i = 0; i < limit; i++)
		if (i < r[i]) swap(A[i], A[r[i]]);
	for (int mid = 1; mid < limit; mid <<= 1) {
		LL Wn = fastpow(type == 1 ? G : Gi, (P - 1) / (mid << 1));
		for (int j = 0; j < limit; j += (mid << 1)) {
			LL w = 1;
			for (int k = 0; k < mid; k++, w = (w * Wn) % P) {
				int x = A[j + k], y = w * A[j + k + mid] % P;
				A[j + k] = (x + y) % P,
					A[j + k + mid] = (x - y + P) % P;
			}
		}
	}
}
int main() {
	scanf("%d%d", &N, &M);
	for (int i = 0; i <= N; i++) scanf("%d", a + i);
	for (int i = 0; i <= M; i++) scanf("%d", b + i);

	while (limit <= N + M) limit <<= 1, L++;
	for (int i = 0; i < limit; i++) r[i] = (r[i >> 1] >> 1) | ((i & 1) << (L - 1));
	NTT(a, 1); NTT(b, 1);
	for (int i = 0; i < limit; i++) a[i] = (a[i] * b[i]) % P;
	NTT(a, -1);
	LL inv = fastpow(limit,	 P - 2);
	for (int i = 0; i <= N + M; i++)
		printf("%d ", (a[i] * inv) % P);
	return 0;
}

C - Triple

題意

給三個陣列 \(A,B,C\) 問有多少個 \((i,j,k)\) 使得

\(A_i,B_j,C_k\) 中較小的兩個數的和大於等於最大的數

\(1 \le T \le 100\)

\(1 \le A_i,B_i,C_i,n \le 100,000\)

There are at most \(20\) test cases with \(N>1000\)

思路

用容斥思想,所有不和法的方案就是 較小的兩數相加小於第三個數的方案。

處理出 所有的 \(A_i + B_j\) ,然後對於每一個 \(C_k\) ,只要加上所有小於 \(C_k\) 的方案數就可以

這裡小資料用暴力,大資料用 多項式乘法

/*
 * @Author: zhl
 * @Date: 2020-11-09 15:23:52
 */
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i = a;i <= b;i++)
#define mem(a,b) memset((a),(b),sizeof(a))

using namespace std;

typedef long long ll;
const double pi = acos(-1.0);
const int N = 6e5 + 10;

struct cp {
	double x, y;
	cp() {}
	cp(double _x, double _y) {
		x = _x; y = _y;
	}
	cp operator + (cp b) {
		return cp(x + b.x, y + b.y);
	}
	cp operator -(cp b) {
		return cp(x - b.x, y - b.y);
	}
	cp operator *(cp b) {
		return cp(x * b.x - y * b.y, x * b.y + y * b.x);
	}
};
int rev[N];
int bit = 0;
int lim;
void FFT(cp* a, int inv) {

	for (int i = 0; i < lim; i++) {
		if (i < rev[i]) {
			swap(a[i], a[rev[i]]);
		}
	}

	for (int mid = 1; mid < lim; mid <<= 1) {
		cp temp(cos(pi / mid), inv * sin(pi / mid));
		for (int i = 0; i < lim; i += mid * 2) {
			cp omega(1, 0);
			for (int j = 0; j < mid; j++, omega = omega * temp) {
				cp x = a[i + j], y = omega * a[i + j + mid];
				a[i + j] = x + y, a[i + j + mid] = x - y;
			}
		}
	}
}


int T, n, A[N], B[N], C[N], tA[N], tB[N], tC[N];
cp x[N], y[N];
ll sum[N];

ll solve_small(int* a, int* b, int* c) {
	for (int i = 0; i <= c[n - 1]; i++)sum[i] = 0;
	for (int i = 0; i < n; i++) {
		for (int j = 0; j < n; j++) {
			sum[a[i] + b[j]]++;
		}
	}
	ll ans = 0;
	for (int i = 1; i <= c[n - 1]; i++)sum[i] += sum[i - 1];
	for (int i = 0; i < n; i++) ans += sum[c[i] - 1];
	return ans;
}

ll solve_big(int* a, int* b, int* c) {

	for (int i = 0; i <= lim; i++)x[i] = cp(a[i], 0), y[i] = cp(b[i], 0);

	FFT(x, 1); FFT(y, 1);
	for (int i = 0; i <= lim; i++)x[i] = x[i] * y[i];
	FFT(x, -1);

	mem(sum, 0);
	ll ans = 0;
	for (int i = 0; i <= c[n - 1]; i++)sum[i] = signed(x[i].x / lim + 0.5);
	for (int i = 1; i <= c[n - 1]; i++)sum[i] += sum[i - 1];
	for (int i = 0; i < n; i++) ans += sum[c[i] - 1];
	return ans;
}

int main() {
	scanf("%d", &T); int c = 0;
	while (T--) {
		scanf("%d", &n);

		mem(tA, 0); mem(tB, 0); mem(tC, 0);
		lim = 1; bit = 0;
		while (lim <= (2 * n))lim <<= 1, bit++;
		mem(rev, 0);
		for (int i = 0; i < lim; i++) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (bit - 1));

		for (int i = 0; i < n; i++)scanf("%d", A + i), tA[A[i]]++; sort(A, A + n);
		for (int i = 0; i < n; i++)scanf("%d", B + i), tB[B[i]]++; sort(B, B + n);
		for (int i = 0; i < n; i++)scanf("%d", C + i), tC[C[i]]++; sort(C, C + n);

		printf("Case #%d: ", ++c);
		if (n <= 1000) {
			printf("%lld\n", 1ll * n * n * n - solve_small(A, B, C) - solve_small(A, C, B) - solve_small(B, C, A));
		}
		else {
			printf("%lld\n", 1ll * n * n * n - solve_big(tA, tB, C) - solve_big(tA, tC, B) - solve_big(tB, tC, A));;
		}
	}
}

5.8 素數測試

複雜度...非常低

Miller_Rabin 判斷素數

Pollard_Rho 分解質因數

/*
 * @Author: zhl
 * @Date: 2020-11-03 11:43:54
 */
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 5;

int x[105];
int mul(int a, int b, int p){
	int ans = 0;
	while (b){
		if (b & 1LL) ans = (ans + a) % p;
		a = (a + a) % p;
		b >>= 1;
	}
	return ans;
}

int qpow(int a, int b, int p){
	int ans = 1;
	while (b){
		if (b & 1LL) ans = mul(ans, a, p);
		a = mul(a, a, p);
		b >>= 1;
	}
	return ans;
}

bool Miller_Rabin(int n){
	if (n == 2) return true;
	int s = 20, i, t = 0;
	int u = n - 1;
	while (!(u & 1))
	{
		t++;
		u >>= 1;
	}
	while (s--)
	{
		int a = rand() % (n - 2) + 2;
		x[0] = qpow(a, u, n);
		for (i = 1; i <= t; i++)
		{
			x[i] = mul(x[i - 1], x[i - 1], n);
			if (x[i] == 1 && x[i - 1] != 1 && x[i - 1] != n - 1)
				return false;
		}
		if (x[t] != 1) return false;
	}
	return true;
}

int gcd(int a, int b){
	return b ? gcd(b, a % b) : a;
}

int Pollard_Rho(int n, int c){
	int i = 1, k = 2, x = rand() % (n - 1) + 1, y = x;
	while (1){
		i++;
		x = (mul(x, x, n) + c) % n;
		int p = gcd((y - x + n) % n, n);
		if (p != 1 && p != n) return p;
		if (y == x) return n;
		if (i == k){
			y = x;
			k <<= 1;
		}
	}
}

map<int, int> m;
void find(int n, int c = 12345)
{
	if (n == 1) return;
	if (Miller_Rabin(n)){
		m[n]++;
		return;
	}
	int p = n, k = c;
	while (p >= n) p = Pollard_Rho(p, c--);
	find(p, k);
	find(n / p, k);
}

int T,n;
int main(){
    cin >> T;
    while(T--){
        cin >> n;
        if(Miller_Rabin(n)){
            cout << "Prime" << endl;
        }else{
            m.clear();
            find(n);
            cout << (*m.rbegin()).first << endl;
        }
        
    }
}


5.9 拉格朗日插值

\(n\) 階的多項式 \(f(x)\) 可以由 \(n+1\) 個點確認。

若現有 \(n+1\) 個點 \((x_i,y_i) \ \ , \ i \in [0,n]\)\(f(x)\)

則可以計算任意值 \(k\) 的函式值 \(f(k)\)

\[f(k) = \sum_{i=0}^ny_i\prod_{j\ne i}\dfrac {k-x_j} {x_i - x_j} \]

例如二次函式 \(f(x)\) 經過了 \((1,4) , (2,9) , (3,16)\) 三個點,則帶入公式中

\[f(k) = 4\dfrac {(k-2)(k-3)}{(1-2)(1-3)} + 9\dfrac{(k-1)(k-3)}{(2-1)(2-3)} + 16 \dfrac {(k-1)(k-2)}{(3-1)(3-2)} \]

不難看出,當 \(k = x_i\) 的時候, \(f(k) = y_i\) ,且\(f(k)\) 是二次函式。

所以正確性可以保證

這樣計算的時間複雜度是 \(O(n^2)\)

特殊情況

\(x_i = i\) , 即 \(n\) 階多項式 \(f(x)\)\(0,1,2,...,n - 1\) 處的函式值

\[f(k) = \sum_{i=0}^ny_i\prod_{j\ne i}\dfrac {k-j} {i - j} \]

我們考慮 \(\prod_{j\ne i}\dfrac {k-j} {i - j}\) 的計算

對於某個 \(k\) ,我們可以預處理出 \(k - j\) 的字首積和字尾積。分母是 \(i!(n-i)!(-1)^{n-i}\)

時間複雜度 \(O(n)\)

例題: 2019南昌邀請賽 B -Polynomial

先算出 \(f(n+1)\) ,然後就有了 \(S(1),...,S(n+1)\) 就可以用 \(S\) 插值計算 \(S(R) - S(L-1)\)

/*
 * @Author: zhl
 * @Date: 2020-11-12 14:50:58
 */
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int mod = 9999991, N = 1e3 + 10;
int y[N], S[N];

int qpow(int a, int p) {
	int ans = 1;
	while (p) {
		if (p & 1)ans = (ans * a) % mod;
		a = (a * a) % mod;
		p >>= 1;
	}
	return ans;
}

int fac[N], invf[N];
void init() {
	fac[0] = invf[0] = 1;
	for (int i = 1; i < N; i++) {
		fac[i] = i * fac[i - 1] % mod;
	}
	invf[N - 1] = qpow(fac[N - 1], mod - 2);
	for (int i = N - 2; i >= 0; i--) {
		invf[i] = invf[i + 1] * (i + 1) % mod;
	}
}
int pre[N], suf[N];

int cal(int* a, int n, int k) {
	pre[0] = k; suf[n + 1] = 1;
	for (int i = 1; i <= n; i++)pre[i] = pre[i - 1] * (k - i) % mod;
	for (int i = n; i >= 0; i--)suf[i] = suf[i + 1] * (k - i) % mod;

	int ans = 0;
	for (int i = 0; i <= n; i++) {
		int f = invf[n - i] * invf[i] % mod;
		if ((n - i) & 1)f = -f;
		ans = (ans + a[i] * f % mod * (i == 0 ? 1 : pre[i - 1]) % mod * suf[i + 1]) % mod;
		if (ans < 0) ans += mod;
	}
	return ans;
}

int T, n, m;
signed main() {
	init();
	scanf("%lld", &T);
	while (T--) {
		scanf("%lld%lld", &n, &m);
		for (int i = 0; i <= n; i++) {
			scanf("%lld", y + i); if (i > 0) S[i] = (S[i - 1] + y[i]) % mod; else S[i] = y[i];
		}
		y[n + 1] = cal(y, n, n + 1);
		S[n + 1] = (S[n] + y[n + 1]) % mod;
		while (m--) {
			int l, r;
			scanf("%lld%lld", &l, &r);
			int ans = cal(S, n + 1, r) - cal(S, n + 1, l - 1);
			if (ans < 0)ans += mod;
			printf("%lld\n", ans % mod);
		}
	}
}
/*
1
3 2
1 10 49 142
6 7
95000 100000
*/

5.10 高斯消元

解多元一次方程組

/*
 * @Author: zhl
 * @Date: 2020-11-14 09:22:58
 */
#include<bits/stdc++.h>
using namespace std;

int n;

struct Gauss {
	double a[110][110];
	int n;
	int solve() {
		for (int i = 1; i <= n; i++) {
			int mx = i;
			for (int j = i + 1; j <= n; j++) {
				if (fabs(a[j][i]) > fabs(a[mx][i])) mx = j;
			}

			for (int j = 1; j <= n + 1; j++)swap(a[i][j], a[mx][j]);
			if (fabs(a[i][i]) < 1e-8) {
				puts("No Solution");
				return -1;
			}
			for (int j = 1; j <= n; j++) {
				if (i == j)continue;
				double t = a[j][i] / a[i][i];
				for (int k = i + 1; k <= n + 1; k++) {
					a[j][k] -= a[i][k] * t;
				}
			}
		}
		
		for (int i = 1; i <= n; i++) {
			a[i][n + 1] /= a[i][i];
			a[i][i] = 1.0;
		}
	}
}G;
int main() {
	scanf("%d", &n);G.n = n;
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= n + 1; j++) {
			scanf("%lf", &G.a[i][j]);
		}
	}
	if (G.solve() != -1){
		for (int i = 1;i <= n;i++) {
			printf("%.2f\n", G.a[i][n + 1]);
		}
	}
}

5.11 盧卡斯

( 不是盧斯卡...

求組合數模數的方法

知乎-演算法學習筆記(25): 盧卡斯定理

\[C_m^n = \dfrac {m!} {n!(m-n)!} \]

一般情況下對一個大素數 \(p\) 取模, 可以線性處理出階乘,階乘的逆元, \(O(1)\) 計算就可以。

const int N = 1e6 + 10;
int fac[N],invfac[N],inv[N];
const int mod = 998244353;

void init(){
    fac[0] = invfac[0] = 1;
    fac[1] = invfac[1] = 1;
    inv[1] = 1;
    for(int i = 2;i < N;i++){
        fac[i] = fac[i-1] * i % mod;
        inv[i] = (mod - mod / i)*inv[mod % i] % mod;
        invfac[i] = invfac[i-1] * inv[i] % mod;
    }
}

int C(int n,int m){
    return fac[n]*invfac[n-m]*invfac[m];
}

但是, 當 \(p < m\) 的時候,就需要用 \(Lucas\)

\[C_m^n = C_{m\%p}^{n\%p} \cdot \ C_{m/p}^{n/p} \ \ (mod\ \ p) \]

【盧卡斯模板】

/*
 * @Author: zhl
 * @Date: 2020-11-12 10:27:57
 */

#include<bits/stdc++.h>
using ll = long long;
using namespace std;

const int N = 1e6 + 10;
ll fac[N], invfac[N], inv[N];


void init(int n, int mod) {
	fac[0] = invfac[0] = 1;
	fac[1] = invfac[1] = 1;
	inv[1] = 1;
	for (int i = 2; i < n; i++) {
		fac[i] = fac[i - 1] * i % mod;
		inv[i] = (mod - mod / i) * inv[mod % i] % mod;
		invfac[i] = invfac[i - 1] * inv[i] % mod;
	}
}


// 需要先預處理出fact[],即階乘
ll C(ll m, ll n, ll p){
	return m < n ? 0 : fac[m] * invfac[n] % p * invfac[m - n] % p;
}
ll lucas(ll m, ll n, ll p){
	return n == 0 ? 1 % p : lucas(m / p, n / p, p) * C(m % p, n % p, p) % p;
}

int T, n, m, k;
int main() {
	scanf("%d", &T);
	while (T--) {
		scanf("%d%d%d", &n, &m, &k);
		init(n + m + 1, k);
		printf("%lld\n", lucas(n + m, n, k));
	}
}

六、資料結構

6.1 線段樹

區間修改區間查詢

/*
 * @Author: zhl
 * @LastEditTime: 2020-12-08 19:35:36
 */
#include<bits/stdc++.h>
#define lo (o<<1)
#define ro (o<<1|1)
#define mid (l+r>>1)
using namespace std;
using ll = long long;

const int N = 1e5 + 10;

ll sum[N << 2], lz[N << 2], A[N];
int n, q;
void build(int o = 1, int l = 1, int r = n) {
	if (l == r) {
		sum[o] = A[l];
		return;
	}
	build(lo, l, mid);build(ro, mid + 1, r);
	sum[o] = sum[lo] + sum[ro];
}
void push_down(int o, int l, int r) {
	if (lz[o] == 0)return;
	lz[lo] += lz[o], lz[ro] += lz[o];
	sum[lo] += lz[o] * (mid - l + 1);
	sum[ro] += lz[o] * (r - mid);
	lz[o] = 0;
}

void updt(int L, int R, ll val, int o = 1, int l = 1, int r = n) {
	if (L <= l and r <= R) {
		lz[o] += val;
		sum[o] += val * (r - l + 1);
		return;
	}
	push_down(o, l, r);
	if (L <= mid)updt(L, R, val, lo, l, mid);
	if (R > mid)updt(L, R, val, ro, mid + 1, r);
	sum[o] = sum[lo] + sum[ro];
}

ll query(int L, int R, int o = 1, int l = 1, int r = n) {
	if (L <= l and r <= R) {
		return sum[o];
	}
	ll ans = 0;
	push_down(o, l, r);
	if (L <= mid)ans += query(L, R, lo, l, mid);
	if (R > mid) ans += query(L, R, ro, mid + 1, r);
	return ans;
}
int main() {
#ifndef ONLINE_JUDGE
	freopen("in.txt", "r", stdin);
#endif

	scanf("%d%d", &n, &q);
	for (int i = 1;i <= n;i++)scanf("%d", A + i);
	build();
	while (q--) {
		int op, a, b, c;
		scanf("%d", &op);
		if (op == 1) {
			scanf("%d%d%d", &a, &b, &c);
			updt(a, b, c);
		}
		else {
			scanf("%d%d", &a, &b);
			printf("%lld\n", query(a, b));
		}
	}
}

掃描線

經典應用,求矩形面積的並

注意下標的差別

區間[l, r] 對應的橫座標分別是 x[l] 和 x[r + 1]

#include<bits/stdc++.h>
#define mid (l+r>>1)
#define lo (o<<1)
#define ro (o<<1|1)
using namespace std;

typedef long long ll;

const int N = 4e5 + 10;

int n;
int nums[N], cnt;

struct line {
	int l, r, h, tag;
	bool operator < (const line& b)const {
		return h < b.h;
	}
}L[N << 1];

ll len[N << 2], times[N << 2];
void push_up(int o, int l, int r) {
	if (times[o])len[o] = nums[r + 1] - nums[l];
	else len[o] = len[lo] + len[ro];
}

void updt(int L, int R, int val, int o = 1, int l = 1, int r = cnt - 1) {
	if (nums[r + 1] <= L or R <= nums[l])return;
	if (L <= nums[l] and nums[r + 1] <= R) {
		times[o] += val;
		push_up(o, l, r);
		return;
	}
	updt(L, R, val, lo, l, mid);
	updt(L, R, val, ro, mid + 1, r);
	push_up(o, l, r);
}
int main() {
#ifdef ONLINE_JUDGE
#else
	freopen("in.txt", "r", stdin);
#endif

	scanf("%d", &n);
	for (int i = 1; i <= n; i++) {
		int lx, ly, rx, ry;
		scanf("%d%d%d%d", &lx, &ly, &rx, &ry);
		nums[++cnt] = lx, nums[++cnt] = rx;
		L[(i << 1) - 1] = { lx,rx,ly,1 };
		L[i << 1] = { lx,rx,ry,-1 };
	}
	sort(L + 1, L + 1 + 2 * n);
	sort(nums + 1, nums + 1 + cnt);
	cnt = unique(nums + 1, nums + 1 + cnt) - nums - 1;

	ll ans = 0;
	for (int i = 1; i <= 2 * n - 1; i++) {
		updt(L[i].l, L[i].r, L[i].tag);
 		ans += len[1] * (L[i + 1].h - L[i].h);
	}
	printf("%lld\n", ans);
}

6.2 樹狀陣列

單點修改,區間查詢

#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 5e5 + 10;
int C[N], n, q, x;
void add(int p, int v) {
	for (; p <= n; p += -p & p)C[p] += v;
}
ll query(int p) {
	ll ans = 0;
	for (; p; p -= -p & p)ans += C[p];
	return ans;
}
int main() {
	scanf("%d%d", &n, &q);
	for (int i = 1; i <= n; i++)scanf("%d", &x), add(i, x);
	while (q--) {
		int op, a, b; scanf("%d%d%d", &op, &a, &b);
		if (op == 1)add(a, b);
		else printf("%lld\n", query(b) - query(a - 1));
	}
}

區間修改,區間查詢

\(A_i\) 是原陣列, \(d_i = A_i - A_{i-1}\)\(A\) 的差分陣列

\[S_n = \sum_{i=1}^nA_i = d_1 + \\d_1 + d_2 +\\ d_1 + d_2 + d_3 + \\ ... \\d_1 + d_2 + d_3 + d_4 + ... + d_n \\ = n (d_1 + d_2 + ... + d_n) - \sum_{i=1}^nd_i(i-1) \]

\(B_i = d_i(i-1)\)

\[S_n = n\sum d - \sum B \]

而當一段區間 \([L,R]\) 一起加上 \(x\) 的時候,

對於 \(d\) 來說,只有 \(d_L\)\(d_{R+1}\) 發生了變化

對於 \(B\) 來說,也是同理,所以可以開兩個樹狀陣列來進行維護

【模板】線段樹1

/*
 * @Author: zhl
 * @Date: 2020-11-17 10:33:57
 */
#include<bits/stdc++.h>
using ll = long long;
using namespace std;

const int N = 1e5 + 10;
int n, m;
struct{
	ll C[N][2]; // 0 是差分d_i , 1 是 d_i * (i - 1)
	void add(int pos, ll val, int o) {
		for (; pos <= n; pos += (-pos) & pos) C[pos][o] += val;
	}
	ll ask(int pos, int o) {
		ll ans = 0;
		for (; pos; pos -= (-pos) & pos) ans += C[pos][o];
		return ans;
	}

	void updt(int l, int r, int x) {
		add(l, x, 0); add(r + 1, -x, 0);
		add(l, x * (l - 1), 1); add(r + 1, -x * (r), 1);
	}
	ll query(int l, int r) {
		ll R = r * ask(r, 0) - ask(r, 1);
		ll L = (l - 1) * ask(l - 1, 0) - ask(l - 1, 1);
		return R - L;
	}
}BIT;

int main() {
	scanf("%d%d", &n, &m);
	int pre = 0;
	for (int i = 1; i <= n; i++) {
		int x; scanf("%d", &x);
		BIT.add(i,x - pre,0);
		BIT.add(i, 1ll * (x - pre)* (i - 1), 1);
		pre = x;
	}
	while (m--) {
		int op, a, b, x;
		scanf("%d", &op);
		if (op == 1) {
			scanf("%d%d%d", &a, &b, &x);
			BIT.updt(a, b, x);
		}
		else {
			scanf("%d%d", &a, &b);
			printf("%lld\n", BIT.query(a, b));
		}
	}
}

6.3 主席樹

也叫可持久化線段樹,這裡線段樹用做了桶。

這個是單點增加,其實就是把一條鏈掛在另一條鏈上

複製資訊,然後開點

這裡是線性的掛鏈,也可以掛成樹形。

#include<bits/stdc++.h>
#define mid (l+r>>1)
using namespace std;

const int maxn = 5e5 + 10;

int sum[maxn << 5], L[maxn << 5], R[maxn << 5];
int cnt;

int a[maxn], id[maxn], root[maxn];

int build(int l, int r) {
	int rt = ++cnt;
	sum[rt] = 0;
	if (l < r) {
		L[rt] = build(l, mid);
		R[rt] = build(mid + 1, r);
	}
	return rt;
}

int updt(int pre, int l, int r, int pos) {
	int rt = ++cnt;
	sum[rt] = sum[pre] + 1;

	R[rt] = R[pre];
	L[rt] = L[pre];

	if (l < r) {
		if (pos <= mid) {
			L[rt] = updt(L[pre], l, mid, pos);
		}
		else {
			R[rt] = updt(R[pre], mid + 1, r, pos);
		}
	}
	return rt;
}

int query(int x, int y, int l, int r, int k) {
	if (l == r) {
		return r;
	}
	int num = sum[L[y]] - sum[L[x]];
	if (num >= k) {
		return query(L[x], L[y], l, mid, k);
	}
	else {
		return query(R[x], R[y], mid + 1, r, k - num);
	}
}

int T, n, m;

int main() {
	//scanf("%d", &T);
	T = 1;
	while (T--) {
		scanf("%d%d", &n, &m);
		cnt = 0;
		for (int i = 1; i <= n; i++) {
			scanf("%d", a + i);
			id[i] = a[i];
		}
		sort(id + 1, id + n + 1);
		int cntID = unique(id + 1, id + n + 1) - (id + 1);
		root[0] = build(1, cntID);

		for (int i = 1; i <= n; i++) {
			int pos = lower_bound(id + 1, id + cntID + 1, a[i]) - id;
			root[i] = updt(root[i - 1], 1, cntID, pos);
		}
		for (int i = 1; i <= m; i++) {
			int x, y, k;
			scanf("%d%d%d", &x, &y, &k);
			int ind = query(root[x - 1], root[y], 1, cntID, k);
			printf("%d\n", id[ind]);
		}
	}
}

6.4 01Tire樹

按二進位制來把一個數掛到樹上對應的鏈,來求解一些二進位制問題

\(01Tire\) 可以用來解決 \(xor\) 問題

struct{
    int c[N][2],tot;
    int getnode(){
        tot++;
        c[tot][0] = c[tot][1] = 0;
        return tot;
    }    
    void insert(int val){
	    int u = 0;
	    for(int i = maxbit;i >= 0;i--){
	        int v = (val & (1 << i) ) ? 1 : 0;
	        if(!c[u][v])c[u][v] = getnode();
	        u = c[u][v];
	    }
    }
    void init(){
        c[0][0] = c[0][1] = 0;
    	tot = 0;
   	}
}Tire;

動態開點

int getnode(){
	tot++;
	c[tot][0] = c[tot][1] = 0;
	return tot;
}  

動態開點的好處就是在多數資料的時候不需要 memset

資料插入

void insert(int val){
	int u = 0;
	for(int i = maxbit;i >= 0;i--){
		int v = (val & (1 << i) ) ? 1 : 0;
		if(!c[u][v])c[u][v] = getnode();
		u = c[u][v];
	}
}

從高位到低位插入到01字典樹中

01Tire

HDU-4825 Xor Sum

給一個集合,每次詢問給出 \(x\) ,輸出集合中的數 \(k\) 使得它們異或最大。

注意是輸出數,不是輸出最大的異或值

思路:

用集合裡的數建 \(01Tire\) ,將 \(x\) 按位取反,從高位開始匹配,匹配失敗則換一邊

#include<bits/stdc++.h>
using namespace std;

const int N = 2e6 + 10;
const int maxbit = 30; //int不能開31,intmax = (1<<31)-1
struct{
    int c[N][2],tot;
    
    int getnode(){
        tot++;
        c[tot][0] = c[tot][1] = 0;
        return tot;
    }    
    void insert(int val){
	    int u = 0;
	    for(int i = maxbit;i >= 0;i--){
	        int v = (val & (1 << i) ) ? 1 : 0;
	        if(!c[u][v])c[u][v] = getnode();
	        u = c[u][v];
	    }
    }

    int query(int val){
        int u = 0;
        int ans = 0;
	    for(int i = maxbit;i >= 0;i--){
	        int v = (val & (1 << i)) ? 0 : 1;
	        if(!c[u][v]) v ^= 1;
	        if(v)ans += (1 << i);
	        
	        u = c[u][v];
	    }
	    return ans;
    }
    void init(){
        c[0][0] = c[0][1] = 0;
    	tot = 0;
   	}
}Tire;

int n,m,x,T;

int main(){
    scanf("%d",&T);
    for(int c = 1;c <= T;c++){
    	Tire.init();
    	scanf("%d%d",&n,&m);
   	 	for(int i = 0;i < n;i++){
   	     	scanf("%d",&x);
   	     	Tire.insert(x);
	   	}
	   	printf("Case #%d:\n",c);
 	   	for(int i = 0;i < m;i++){
        	scanf("%d",&x);
        	printf("%d\n",Tire.query(x));
	   	}
    }
}

可持久化Tire

51nod-1295 XOR key

區間查詢

搞一個可持久化的Tire就好了

插入:

// root[i] = insert(root[i-1],v,val);
int insert(int pre, int v, int val) {
	int u = getnode();
	int ans = u;
	for (int i = maxbit; i >= 0; i--) {
		c[u][0] = c[pre][0];
		c[u][1] = c[pre][1]; //複製資訊
		int x = val & (1 << i) ? 1 : 0;
		c[u][x] = getnode();//開點
		u = c[u][x]; // 0 是空的節點,所以可以一直這樣迭代
		pre = c[pre][x];
	}
	return ans;
}

其實也很簡單,就是複製之前的資訊,然後開點。

查詢:

由於開點是一個一個分配的,所以只要 id < root[l] 就不是區間內

,所以只要加上這一個判斷就跟之前的一樣。

int query(int l, int r, int x) {
	int MinID = root[l];

	int u = root[r];
	int ans = 0;
	for (int i = maxbit; i >= 0; i--) {
		int now = (x & (1 << i)) ? 0 : 1;
		if (c[u][now] and c[u][now] >= MinID) {
			u = c[u][now];
			ans += (1 << i);
		}
		else {
			u = c[u][now ^ 1];
		}
	}
	return ans;
}

完整程式碼

/*
 * @Author: zhl
 * @Date: 2020-10-13 09:46:47
 */
                                                           
#include<bits/stdc++.h>
using namespace std;

#define rep(i,a,b) for(int i = a;i <= b;i++)
#define repE(i,u) for(int i = head[u];i;i = E[i].next)
#define swap(a,b) a^=b,b^=a,a^=b

const int N = 2e6 + 10;
const int maxbit = 30;
struct {
	int root[N], c[N][2], tot;
	void init() {
		c[0][0] = c[0][1] = 0;
		tot = 0;
		root[0] = 0;
	}
	int getnode() {
		tot++;
		c[tot][0] = c[tot][1] = 0;
		return tot;
	}
	//root[v] = insret(Tire.root[v-1], v, val);
	int insert(int pre, int v, int val) {
		int u = getnode();
		int ans = u;
		for (int i = maxbit; i >= 0; i--) {
			c[u][0] = c[pre][0];
			c[u][1] = c[pre][1];
			int x = val & (1 << i) ? 1 : 0;
			c[u][x] = getnode();
			u = c[u][x];
			pre = c[pre][x];
		}
		return ans;
	}
	int query(int l, int r, int x) {
		int Treesize = maxbit + 2;
		int MinID = root[l];

		int u = root[r];
		int ans = 0;
		for (int i = maxbit; i >= 0; i--) {
			int now = (x & (1 << i)) ? 0 : 1;
			if (c[u][now] and c[u][now] >= MinID) {
				u = c[u][now];
				ans += (1 << i);
			}
			else {
				u = c[u][now ^ 1];
			}
		}
		return ans;
	}
}Tire;

int n, m, l, r, x;
int A[N];
int main() {
	scanf("%d%d", &n, &m);
	Tire.init();
	for (int i = 1; i <= n; i++) {
		scanf("%d", A + i);
		Tire.root[i] = Tire.insert(Tire.root[i-1],i,A[i]);
	}
	for (int i = 1; i <= m; i++) {
		scanf("%d%d%d", &x, &l, &r);
		printf("%d\n", Tire.query(l + 1, r + 1, x));
	}
}

6.5 Treap

Tree + Heap

\(Treap\) 是平衡樹的一種,\(Treap\)\(Tree\)\(Heap\) 的組合

核心思想

每個節點有兩個屬性 \(key\)\(val\) 其中的 key 滿足 BST 二叉搜尋樹的性質,其中序遍歷是一個有序序列。val 滿足 Heap 堆的性質,一個節點的 \(val\) 值大於其孩子節點的 \(val\)

隨機分配 \(val\) 使得 期望的 \(BST\) 是平均深度的

結構定義

struct Node{
    int l, r;
    int key, val;//key是BST關鍵字, val是Heap關鍵字
    int cnt, size;
}tr[N];

int root, idx;

節點分配

int get_node(int key){
    tr[ ++ idx].key = key;
    tr[idx].val = rand(); //隨機資料
    tr[idx].cnt = tr[idx].size = 1;
    return idx;
}

插入操作

void insert(int &p, int key)
{
    if (!p) p = get_node(key);
    else if (tr[p].key == key) tr[p].cnt ++ ;
    else if (tr[p].key > key)
    {
        insert(tr[p].l, key);
        if (tr[tr[p].l].val > tr[p].val) zig(p);//右旋
    }
    else
    {
        insert(tr[p].r, key);
        if (tr[tr[p].r].val > tr[p].val) zag(p);//左旋
    }
    pushup(p);
}

刪除操作

void remove(int &p, int key)
{
    if (!p) return;
    if (tr[p].key == key)
    {
        if (tr[p].cnt > 1) tr[p].cnt -- ;
        else if (tr[p].l || tr[p].r)
        {
            if (!tr[p].r || tr[tr[p].l].val > tr[tr[p].r].val)
            {
                zig(p);
                remove(tr[p].r, key);
            }
            else
            {
                zag(p);
                remove(tr[p].l, key);
            }
        }
        else p = 0;
    }
    else if (tr[p].key > key) remove(tr[p].l, key);
    else remove(tr[p].r, key);

    pushup(p);
}

旋轉操作

void zig(int &p)    // 右旋
{
    int q = tr[p].l;
    tr[p].l = tr[q].r, tr[q].r = p, p = q;
    pushup(tr[p].r), pushup(p);
}

void zag(int &p)    // 左旋
{
    int q = tr[p].r;
    tr[p].r = tr[q].l, tr[q].l = p, p = q;
    pushup(tr[p].l), pushup(p);
}

模板

普通平衡樹

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010, INF = 1e8;

int n;
struct Node
{
    int l, r;
    int key, val;//key是BST關鍵字, val是Heap關鍵字
    int cnt, size;
}tr[N];

int root, idx;

void pushup(int p)
{
    tr[p].size = tr[tr[p].l].size + tr[tr[p].r].size + tr[p].cnt;
}

int get_node(int key)
{
    tr[ ++ idx].key = key;
    tr[idx].val = rand(); //隨機資料
    tr[idx].cnt = tr[idx].size = 1;
    return idx;
}

void zig(int &p)    // 右旋
{
    int q = tr[p].l;
    tr[p].l = tr[q].r, tr[q].r = p, p = q;
    pushup(tr[p].r), pushup(p);
}

void zag(int &p)    // 左旋
{
    int q = tr[p].r;
    tr[p].r = tr[q].l, tr[q].l = p, p = q;
    pushup(tr[p].l), pushup(p);
}

void build()
{
    get_node(-INF), get_node(INF);
    root = 1, tr[1].r = 2;
    pushup(root);
    if (tr[1].val < tr[2].val) zag(root);
}


void insert(int &p, int key)
{
    if (!p) p = get_node(key);
    else if (tr[p].key == key) tr[p].cnt ++ ;
    else if (tr[p].key > key)
    {
        insert(tr[p].l, key);
        if (tr[tr[p].l].val > tr[p].val) zig(p);
    }
    else
    {
        insert(tr[p].r, key);
        if (tr[tr[p].r].val > tr[p].val) zag(p);
    }
    pushup(p);
}

void remove(int &p, int key)
{
    if (!p) return;
    if (tr[p].key == key)
    {
        if (tr[p].cnt > 1) tr[p].cnt -- ;
        else if (tr[p].l || tr[p].r)
        {
            if (!tr[p].r || tr[tr[p].l].val > tr[tr[p].r].val)
            {
                zig(p);
                remove(tr[p].r, key);
            }
            else
            {
                zag(p);
                remove(tr[p].l, key);
            }
        }
        else p = 0;
    }
    else if (tr[p].key > key) remove(tr[p].l, key);
    else remove(tr[p].r, key);

    pushup(p);
}

int get_rank_by_key(int p, int key)    // 通過數值找排名
{
    if (!p) return 0;   // 本題中不會發生此情況
    if (tr[p].key == key) return tr[tr[p].l].size + 1;
    if (tr[p].key > key) return get_rank_by_key(tr[p].l, key);
    return tr[tr[p].l].size + tr[p].cnt + get_rank_by_key(tr[p].r, key);
}

int get_key_by_rank(int p, int rank)   // 通過排名找數值
{
    if (!p) return INF;     // 本題中不會發生此情況
    if (tr[tr[p].l].size >= rank) return get_key_by_rank(tr[p].l, rank);
    if (tr[tr[p].l].size + tr[p].cnt >= rank) return tr[p].key;
    return get_key_by_rank(tr[p].r, rank - tr[tr[p].l].size - tr[p].cnt);
}

int get_prev(int p, int key)   // 找到嚴格小於key的最大數
{
    if (!p) return -INF;
    if (tr[p].key >= key) return get_prev(tr[p].l, key);
    return max(tr[p].key, get_prev(tr[p].r, key));
}

int get_next(int p, int key)    // 找到嚴格大於key的最小數
{
    if (!p) return INF;
    if (tr[p].key <= key) return get_next(tr[p].r, key);
    return min(tr[p].key, get_next(tr[p].l, key));
}

int main()
{
    build();

    scanf("%d", &n);
    while (n -- )
    {
        int opt, x;
        scanf("%d%d", &opt, &x);
        if (opt == 1) insert(root, x);
        else if (opt == 2) remove(root, x);
        else if (opt == 3) printf("%d\n", get_rank_by_key(root, x) - 1);
        else if (opt == 4) printf("%d\n", get_key_by_rank(root, x + 1));
        else if (opt == 5) printf("%d\n", get_prev(root, x));
        else printf("%d\n", get_next(root, x));
    }

    return 0;
}

6.6 Splay

Splay 是平衡樹

\(Splay\) 是平衡樹的一種

基本思想是, 對於查詢頻率較高的節點,使其處於離根節點相對較近的節點

Spaly的基本操作有

  • rotate(旋轉)
  • splay (伸展)
  • push_up
  • push_down
struct Node {
	int son[2], fa, val;
	int size, flag;

	void init(int _val, int _fa) {
		val = _val; fa = _fa;
		size = 1;
	}
}tr[N];

rotate()

這個旋轉操作跟資料結構裡學的平衡樹旋轉操作是一樣的。

如下圖,畫的是右旋 \(x\) 的操作,藍色的邊表示資訊發生了改變

void rotate(int x) {
	int y = tr[x].fa, z = tr[y].fa;
	int k = (tr[y].son[1] == x); // k = 0 表示 x 是 y 的左兒子
	tr[z].son[tr[z].son[1] == y] = x, tr[x].fa = z;
	tr[y].son[k] = tr[x].son[k ^ 1], tr[tr[x].son[k ^ 1]].fa = y;
	tr[x].son[k ^ 1] = y, tr[y].fa = x;
	push_up(y), push_up(x);
}

程式碼中修改資訊的三行對應了三條藍邊,上圖表示的情況是 \(k=0\)

splay()

介面 splay(int x,int k) 把節點 \(u\) 轉到 \(k\) 下面,若 \(k\)\(0\) , 則轉到根的位置

void splay(int x, int k) {
	while (tr[x].fa != k) {
		int y = tr[x].fa, z = tr[y].fa;
		if (z != k) {
			if ((tr[y].son[1] == x) ^ (tr[z].son[1] == y))rotate(x);
			else rotate(y); //轉y才是log複雜度
		}
		rotate(x);
	}
	if (!k)root = x;
}

if ((tr[y].son[1] == x) ^ (tr[z].son[1] == y)) 表示的是不是鏈狀,此時轉兩下 \(x\) ,若是鏈狀,則需要先轉一下 \(y\) 再轉 \(x\) ,不能轉兩次 \(x\)

push_up()

類似線段樹的 push_up() ,一般只需要維護 \(size\)

void push_up(int u) {
	tr[u].size = tr[tr[u].son[0]].size + tr[tr[u].son[1]].size + 1;
}

push_down()

在需要 \(lazytag\) 時將標記下傳

洛谷模板

區間翻轉

/*
 * @Author: zhl
 * @Date: 2020-11-06 14:24:16
 */


#include<bits/stdc++.h>
using namespace std;

const int N = 1e5 + 10;
int n, m;;

struct Node {
	int son[2], fa, val;
	int size, flag;

	void init(int _val, int _fa) {
		val = _val; fa = _fa;
		size = 1;
	}
}tr[N];

int root, idx;


void push_up(int u) {
	tr[u].size = tr[tr[u].son[0]].size + tr[tr[u].son[1]].size + 1;

}
void push_donw(int u) {
	if (tr[u].flag) {
		swap(tr[u].son[0], tr[u].son[1]);
		tr[tr[u].son[0]].flag ^= 1;
		tr[tr[u].son[1]].flag ^= 1;
		tr[u].flag = 0;
	}
}

void rotate(int x) {
	int y = tr[x].fa, z = tr[y].fa;
	int k = (tr[y].son[1] == x); // k = 0 表示 x 是 y 的左兒子
	tr[z].son[tr[z].son[1] == y] = x, tr[x].fa = z;
	tr[y].son[k] = tr[x].son[k ^ 1], tr[tr[x].son[k ^ 1]].fa = y;
	tr[x].son[k ^ 1] = y, tr[y].fa = x;
	push_up(y), push_up(x);
}

void splay(int x, int k) {
	while (tr[x].fa != k) {
		int y = tr[x].fa, z = tr[y].fa;
		if (z != k) {
			if ((tr[y].son[1] == x) ^ (tr[z].son[1] == y))rotate(x);
			else rotate(y); //
		}
		rotate(x);
	}
	if (!k)root = x;
}

void insert(int val) {
	int u = root, p = 0;
	while (u) p = u, u = tr[u].son[val > tr[u].val];
	u = ++idx;
	if (p) tr[p].son[val > tr[p].val] = u;
	tr[u].init(val, p);
	splay(u, 0);
}

int get_k(int k) {
	int u = root;
	while (1) {
		push_donw(u);
		if (tr[tr[u].son[0]].size >= k) u = tr[u].son[0];
		else if (tr[tr[u].son[0]].size + 1 == k)return u;
		else k -= tr[tr[u].son[0]].size + 1, u = tr[u].son[1];
	}
}

void output(int u) {
	push_donw(u);
	if (tr[u].son[0]) output(tr[u].son[0]);
	if (tr[u].val >= 1 and tr[u].val <= n)printf("%d ", tr[u].val);
	if (tr[u].son[1]) output(tr[u].son[1]);
}

int main() {
	scanf("%d%d", &n, &m);
	for (int i = 0; i <= n + 1; i++)insert(i);
	while (m--) {
		int l, r;
		scanf("%d%d", &l, &r);
		l = get_k(l), r = get_k(r + 2);
		splay(l, 0); splay(r, l);
		tr[tr[r].son[0]].flag ^= 1;
	}
	output(root);
}

永無鄉

啟發式合併

/*
 * @Author: zhl
 * @Date: 2020-11-11 15:27:48
 */
#include<bits/stdc++.h>
using namespace std;

const int N = 5e5 + 10;

struct Node {
	int v, s[2], p, id, size;
	void init(int _v, int _id, int _p) {
		v = _v; id = _id; p = _p; size = 1;
	}
}tr[N];
int idx, root[N];
int n, m;

int fa[N];

int find(int a) {
	return a == fa[a] ? a : fa[a] = find(fa[a]);
}
void push_up(int x) {
	tr[x].size = tr[tr[x].s[0]].size + tr[tr[x].s[1]].size + 1;
}

void rotate(int x) {
	int y = tr[x].p, z = tr[y].p;
	int k = tr[y].s[1] == x;
	tr[z].s[tr[z].s[1] == y] = x; tr[x].p = z;
	tr[y].s[k] = tr[x].s[k ^ 1]; tr[tr[x].s[k ^ 1]].p = y;
	tr[x].s[k ^ 1] = y; tr[y].p = x;
	push_up(y), push_up(x);
}


void splay(int x, int k, int rt) {
	while (tr[x].p != k) {
		int y = tr[x].p, z = tr[y].p;
		if (z != k) {
			if ((tr[z].s[0] == y) ^ (tr[y].s[0] == x))rotate(x);
			else rotate(y);
		}
		rotate(x);
	}
	if (!k)root[rt] = x;
}

void insert(int v, int id, int rt) {
	int u = root[rt], p = 0;
	while (u) p = u, u = tr[u].s[v > tr[u].v];
	u = ++idx;
	if (p)tr[p].s[v > tr[p].v] = u;
	tr[u].init(v, id, p);
	splay(u, 0, rt);
}

int get_k(int k, int rt) {
	int u = root[rt];
	while (u) {
		if (tr[tr[u].s[0]].size >= k) u = tr[u].s[0];
		else if (tr[tr[u].s[0]].size + 1 == k)return tr[u].id;
		else k -= tr[tr[u].s[0]].size + 1, u = tr[u].s[1];
	}
	return -1;
}

void dfs(int a, int b) {
	if (tr[a].s[0])dfs(tr[a].s[0], b);
	if (tr[a].s[1])dfs(tr[a].s[1], b);
	insert(tr[a].v, tr[a].id, b);
}
int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++) {
		fa[i] = root[i] = i;
		int v; scanf("%d", &v);
		tr[i].init(v, i, 0);
	}
	idx = n;
	for (int i = 1; i <= m; i++) {
		int a, b; scanf("%d%d", &a, &b);
		a = find(a); b = find(b);
		if (a != b) {
			if (tr[root[a]].size > tr[root[b]].size)swap(a, b);
			dfs(root[a], b);
			fa[a] = b;
		}
	}
	scanf("%d", &m);
	while (m--) {
		char op[2]; int a, b;
		scanf("%s%d%d", op, &a, &b);
		if (*op == 'B') {
			a = find(a); b = find(b);
			if (a != b) {
				if (tr[root[a]].size > tr[root[b]].size)swap(a, b);
				dfs(root[a],b);
				fa[a] = b;
			}
		}
		else {
			a = find(a);
			if (tr[root[a]].size < b)puts("-1");
			else {
				printf("%d\n", get_k(b, a));
			}
		}
	}
}

6.7 塊狀連結串列

對於線性表,可以 \(O(1)\) 的訪問,但是插入和刪除操作是 \(O(n)\)

對於連結串列,可以 \(O(1)\) 的進行插入和刪除,但是是 \(O(n)\) 的訪問。

於是本著分塊的思想,有了塊狀連結串列

大概長這個樣子。每個塊的大小數量級在 \(O(\sqrt n)\) , 塊數的量級 \(O(\sqrt n)\)

主要有以下幾種操作:

插入

(1) 分裂節點 \(O(\sqrt n)\)

(2) 在分裂點插入 \(O(\sqrt n)\)

刪除

(1) 刪除開頭節點的後半部分 \(O(\sqrt n)\)

(2) 刪除中心完整節點 \(O(\sqrt n)\)

(3) 刪除結尾節點的前半部分 \(O(\sqrt n)\)

合併

為了保證正確的複雜度,要不定期的進行合併操作。

所謂合併操作,就是從第一個塊開始,如果把下一個塊合併過來之後大小不大於 $ \sqrt n$ ,就把兩個塊合併

若沒有合併操作,則可能會有很多小塊,導致 \(TLE\)

rope

STL 中帶的塊狀連結串列,內部好像是平衡樹實現的

【2020牛客國慶派對Day8】 G. Shuffle Cards

題意

\(n,m\)

初始狀態為 \(1,2,3,...,n\)

\(m\) 次操作,每次操作從 \(pos\) 出開始,取 \(l\) 長度,把這一段取出放到最前面

問最後的狀態

#include <bits/stdc++.h>
#include <ext/rope>

using namespace std;
using namespace __gnu_cxx;
const int N = 100010;

int main() {
	int n, m, a[N] = {0}; cin >> n >> m;
	for (int i = 0; i < n; i++) a[i] = i + 1;
	rope<int> rp(a);
	while (m--) {
		int p, s; cin >> p >> s; p--;
		rp.insert(0, rp.substr(p, s));
		rp.erase(p + s, s);
	}
	for (auto i : rp) cout << i << " ";
	return 0;
}

程式碼

文字編輯器

/*
 * @Author: zhl
 * @Date: 2020-11-18 11:30:27
 */
#include<bits/stdc++.h>
#pragma GCC optimize(2)
using namespace std;

const int N = 2e3 + 10, M = 2e3 + 10;

struct node {
	char s[N + 1];
	int c, l, r;
}p[M];
int id[N], idx;//可分配的編號池
char str[2000010];
int n, x, y;
void move(int k)//移動到第k個字元後面
{
	x = p[0].r;
	while (k > p[x].c) k -= p[x].c, x = p[x].r;
	y = k - 1;
}
void add(int x, int u) //將節點u插到節點x的右邊
{
	p[u].r = p[x].r; p[p[u].r].l = u;
	p[x].r = u; p[u].l = x;
}
void del(int u) //刪除節點u
{
	p[p[u].l].r = p[u].r;
	p[p[u].r].l = p[u].l;
	p[u].l = p[u].r = p[u].c = 0;
	id[++idx] = u;
}
void insert(int k) //在游標後面插入k個字元
{
	if (y + 1 != p[x].c) //分裂
	{
		int u = id[idx--];
		for (int i = y + 1; i < p[x].c; i++) p[u].s[p[u].c++] = p[x].s[i];
		p[x].c = y + 1;
		add(x, u);
	}
	int cur = x, i = 0;
	while (i < k) {
		int u = id[idx--];
		for (; i < k and p[u].c < N; i++) {
			p[u].s[p[u].c++] = str[i];
		}
		add(cur, u);
		cur = u;
	}
}
void remove(int k) //刪除游標後的k個字元
{
	if (y + 1 + k <= p[x].c) {
		for (int i = y + 1, j = y + 1 + k; j < p[x].c; j++,i++) {
			p[x].s[i] = p[x].s[j];
		}
		p[x].c -= k;
	}
	else {
		k -= (p[x].c - y - 1);
		p[x].c = y + 1;
		while (p[x].r and k >= p[p[x].r].c) {
			k -= p[p[x].r].c;
			del(p[x].r);
		}
		int u = p[x].r;
		for (int i = 0, j = k; j < p[u].c; j++, i++)p[u].s[i] = p[u].s[j];
		p[u].c -= k;
	}
}

void get(int k) //獲取游標後k個字母
{
	if (y + 1 + k <= p[x].c) {
		for (int i = y + 1; i <= y + k; i++)putchar(p[x].s[i]);
	}
	else {
		k -= (p[x].c - y - 1);
		for (int i = y + 1; i < p[x].c; i++)putchar(p[x].s[i]);
		int cur = x;
		while (p[cur].r and k >= p[p[cur].r].c) {
			k -= p[p[cur].r].c;
			for (int i = 0; i < p[p[cur].r].c; i++)putchar(p[p[cur].r].s[i]);
			cur = p[cur].r;
		}
		int u = p[cur].r;
		for (int i = 0; i < k; i++)putchar(p[u].s[i]);
	}
	puts("");
}
void prev() //游標前移
{
	if (y) y--;
	else {
		x = p[x].l;
		y = p[x].c - 1;
	}
}
void next() //游標後移
{
	if (y != p[x].c - 1) {
		y++;
	}
	else {
		x = p[x].r;
		y = 0;
	}
}

void merge() //關鍵操作,將長度較短的合併,保持正確的複雜度
{
	for (int i = p[0].r; i; i = p[i].r) {
		while (p[i].r and p[i].c + p[p[i].r].c < N) {
			int r = p[i].r;	
			for (int ii = p[i].c, j = 0; j < p[r].c; ii++, j++) {
				p[i].s[ii] = p[r].s[j];
			}
			if (x == r) x = i, y += p[i].c;
			p[i].c += p[r].c;
			del(r);
		}
	}
}
int main() {
	for (int i = 1; i < M; i++) id[++idx] = i;
	scanf("%d", &n);
	char op[10];

	str[0] = '>';
	insert(1);  // 插入哨兵
	move(1);  // 將游標移動到哨兵後面

	while (n--)
	{
		int a;
		scanf("%s", op);
		if (!strcmp(op, "Move"))
		{
			scanf("%d", &a);
			move(a + 1);
		}
		else if (!strcmp(op, "Insert"))
		{
			scanf("%d", &a);
			int i = 0, k = a;
			while (a)
			{
				str[i] = getchar();
				if (str[i] >= 32 && str[i] <= 126) i++, a--;
			}
			insert(k);
			merge();
		}
		else if (!strcmp(op, "Delete"))
		{
			scanf("%d", &a);
			remove(a);
			merge();
		}
		else if (!strcmp(op, "Get"))
		{
			scanf("%d", &a);
			get(a);
		}
		else if (!strcmp(op, "Prev")) prev();
		else next();
	}
}


rope 版本

/*
 * @Author: zhl
 * @Date: 2020-11-18 10:28:37
 */
#include <bits/stdc++.h>
#include <ext/rope>

using namespace std;
using namespace __gnu_cxx;
char str[2000010];
int main(){
    rope<char> rp;int pos = 0;
    int n;
	scanf("%d", &n);
	char op[10];
	while (n--)
	{
		int a;
		scanf("%s", op);
		if (!strcmp(op, "Move"))
		{
			scanf("%d", &pos);
		}
		else if (!strcmp(op, "Insert"))
		{
			scanf("%d", &a);
            str[a] = '\0';// Important!!!
			int i = 0, k = a;
			while (a)
			{
				str[i] = getchar();
				if (str[i] >= 32 && str[i] <= 126) i++, a--;
			}
			rp.insert(pos,str);
		}
		else if (!strcmp(op, "Delete"))
		{
			scanf("%d", &a);
			rp.erase(pos,a);
		}
		else if (!strcmp(op, "Get"))
		{
			scanf("%d", &a);a--;
			for(int i = pos;i <= pos + a;i++)putchar(rp[i]);
            puts("");
		}
		else if (!strcmp(op, "Prev")) pos--;
		else pos++;
	}
}

6.8 莫隊

分塊思想

分塊思想其實是一種暴力

還是 【線段樹1】洛谷模板

我們可以把它分成 \(\sqrt n, \sqrt n,..., \sqrt n\) 這樣的一個一個塊。

#include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e5 + 10, M = 350;
int id[N], w[N];
ll sum[M], lz[M];

int n, m, len;
void updt(int l, int r, int v) {
	if (id[l] == id[r]) {
		for (int i = l; i <= r; i++) w[i] += v, sum[id[i]] += v;
		return;
	}
	int i = l, j = r;
	while (id[i] == id[l])w[i] += v, sum[id[i]] += v, i++;
	while (id[j] == id[r])w[j] += v, sum[id[j]] += v, j--;

	for (int k = id[i]; k <= id[j]; k++)sum[k] += 1ll * len * v, lz[k] += v;
}
ll query(int l, int r) {
	ll res = 0;
	if (id[l] == id[r]) {
		for (int i = l; i <= r; i++) res += w[i] + lz[id[i]];
		return res;
	}
	int i = l, j = r;
	while (id[i] == id[l]) res += w[i] + lz[id[i]], i++;
	while (id[j] == id[r]) res += w[j] + lz[id[j]], j--;
	for (int k = id[i]; k <= id[j]; k++) res += sum[k];
	return res;
}
int main() {
	scanf("%d%d", &n, &m);
	len = sqrt(n);
	for (int i = 1; i <= n; i++) scanf("%d", w + i), id[i] = i / len, sum[id[i]] += w[i];

	while (m--) {
		int op, a, b, c;
		scanf("%d", &op);
		if (op == 1) {
			scanf("%d%d%d", &a, &b, &c);
			updt(a, b, c);
		}
		else {
			scanf("%d%d", &a, &b);
			printf("%lld\n", query(a, b));
		}
	}
}

普通莫隊

求區間不同數的個數

/*
 * @Author: zhl
 * @LastEditTime: 2020-12-09 10:31:44
 */
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;

int n, q, A[N], cnt[N], ID[N];
struct query {
	int l, r, id;
	bool operator < (const query& b)const {
		return ID[l] == ID[b.l] ? r < b.r : ID[l] < ID[b.l];
	}
}Q[N];
int res, ans[N];
void del(int idx) {
	if (!--cnt[A[idx]])res--;
}
void add(int idx) {
	if (!cnt[A[idx]]++)res++;
}
int main() {
	scanf("%d", &n); int t = sqrt(n);
	for (int i = 1; i <= n; i++)scanf("%d", A + i), ID[i] = i / t;
	scanf("%d", &q);
	for (int i = 1; i <= q; i++)scanf("%d%d", &Q[i].l, &Q[i].r), Q[i].id = i;
	sort(Q + 1, Q + 1 + q);
	int l = 1, r = 0;
	for (int i = 1; i <= q; i++) {
		int ql = Q[i].l, qr = Q[i].r;
		while (l < ql)del(l++);
		while (l > ql)add(--l);
		while (r < qr)add(++r);
		while (r > qr)del(r--);
		ans[Q[i].id] = res;
	}
	for (int i = 1; i <= q; i++)printf("%d\n", ans[i]);
}

帶修改的莫隊

【數顏色】

墨墨購買了一套N支彩色畫筆(其中有些顏色可能相同),擺成一排,你需要回答墨墨的提問。墨墨會向你釋出如下指令:

1、 Q L R代表詢問你從第L支畫筆到第R支畫筆中共有幾種不同顏色的畫筆。

2、 R P Col 把第P支畫筆替換為顏色Col。

為了滿足墨墨的要求,你知道你需要幹什麼了嗎?

就是在基礎的莫隊上增加了修改操作

所以需要挪動三個指標 \(L\) , \(R\) , \(t\)

有個小技巧

while (t < qt) {
    t++;
    if (ql <= m[t].pos and m[t].pos <= qr) {
        del(A[m[t].pos]);
        add(m[t].val);
    }
    swap(A[m[t].pos], m[t].val);
}
while (t > qt) {
    if (ql <= m[t].pos and m[t].pos <= qr) {
        del(A[m[t].pos]);
        add(m[t].val);
    }
    swap(A[m[t].pos], m[t].val);
    t--;
}

這個 \(swap\) 操作就很靈性

塊大小為 \(^3\sqrt {nt}\) 的時候達到理論最快複雜度, 然而我 \(TLE\) 了 wrnm

我這份程式碼 len = cbrt(1.0 * n * mcnt) + 1; 會被卡一個點

前兩個塊大小 \(n ^ {\frac 2 3}\)\(n^{\frac 3 4}\) 都可以通過, 0.75跑的最快。

改成第三份理論最優就 TLE ? 難道又是我的毒瘤程式碼的鍋

//len = pow(n ,0.6667);
len = pow(n, 0.75);
//len = pow(n * mcnt, 0.333) + 1;
/*
 * @Author: zhl
 * @Date: 2020-11-19 10:39:02
 */
#include<bits/stdc++.h>
using namespace std;

const int N = 150000;
int A[N], cnt[1000010], block[N];
int n, mm, len;
struct query {
	int id, l, r, t;
	bool operator < (const query& rhs)const {
		int al = block[l], bl = block[rhs.l];
		int ar = block[r], br = block[rhs.r];
		if (al != bl)return al < bl;
		if (ar != br)return ar < br;
		return t < rhs.t;
	}
}q[N];
struct modify {
	int pos, val;
}m[N];
long long qcnt, mcnt, ans[N], now;

void del(int x) {
	if (!--cnt[x])now--;
}
void add(int x) {
	if (!cnt[x]++)now++;
}
signed main() {
	scanf("%d%d", &n, &mm);
	for (int i = 1; i <= n; i++)scanf("%d", A + i);

	while (mm--) {
		char op[2]; int a, b;
		scanf("%s%d%d", op, &a, &b);
		if (*op == 'Q') {
			qcnt++;
			q[qcnt] = { qcnt,a,b, mcnt };
			//q[qcnt] = { ++qcnt,a,b,mcnt };
		}
		else {
			m[++mcnt] = { a,b };
		}
	}
	len = pow(n ,0.6667);
	for (int i = 1; i <= n; i++)block[i] = i / len;
	sort(q + 1, q + 1 + qcnt);
	int l = 1, r = 0, t = 0;

	for (int i = 1; i <= qcnt; i++) {
		int ql = q[i].l, qr = q[i].r, qt = q[i].t;
		while (l < ql) del(A[l++]);
		while (l > ql) add(A[--l]);
		while (r < qr) add(A[++r]);
		while (r > qr) del(A[r--]);

		while (t < qt) {
			t++;
			if (ql <= m[t].pos and m[t].pos <= qr) {
				del(A[m[t].pos]);
				add(m[t].val);
			}
			swap(A[m[t].pos], m[t].val);
		}
		while (t > qt) {
			if (ql <= m[t].pos and m[t].pos <= qr) {
				del(A[m[t].pos]);
				add(m[t].val);
			}
			swap(A[m[t].pos], m[t].val);
			t--;
		}
		ans[q[i].id] = now;
	}
	for (int i = 1; i <= qcnt; i++)printf("%d\n", ans[i]);
}

回滾莫隊

AT1219 歴史の研究 - 洛谷

題意

查詢區間 \([l,r]\) 內一個數乘上它在區間出現次數的最大值

使用莫隊的時候進行增加操作的時候會很簡單,但是在刪除操作的時候不是那麼好維護的時候,可以使用不刪除的莫隊(回滾莫隊)

還是相同的思路,先把詢問排序

然後對於左端點在同一個塊的詢問來說

如圖

如果右端點也在塊內,則暴力計算

否則左端點從下一個塊的左邊開始,右端點單調向右移動。

左端點在塊內反覆進行回滾操作。

這樣就在保證時間複雜度還是 \(O(n\sqrt n)\) 的情況下避免了刪除操作

/*
 * @Author: zhl
 * @Date: 2020-11-19 10:38:35
 */
 #include<bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1e5 + 10;
int n, m, len, cnt[N], nums[N], w[N], ID[N];
ll ans[N];
struct Query {
	int id, l, r;
	bool operator < (const Query& rhs)const {
		int al = ID[l], bl = ID[rhs.l];
		if (al != bl)return al < bl;
		return r < rhs.r;
	}
}q[N];

void add(int x, ll& res) {
	cnt[x]++;
	res = max(res, 1ll * cnt[x] * nums[x]);
}

int main() {
	scanf("%d%d", &n, &m);
	int numID = 0;
	for (int i = 1; i <= n; i++)scanf("%d", w + i), nums[++numID] = w[i];

	sort(nums + 1, nums + 1 + n);
	numID = unique(nums + 1, nums + 1 + n) - nums - 1;
	len = sqrt(n);
	for (int i = 1; i <= n; i++)ID[i] = i / len;
	for (int i = 1; i <= n; i++)w[i] = lower_bound(nums + 1, nums + 1 + numID, w[i]) - nums;

	for (int i = 1; i <= m; i++) {
		scanf("%d%d", &q[i].l, &q[i].r);
		q[i].id = i;
	}

	sort(q + 1, q + 1 + m);

	for (int x = 1; x <= m;) {
		int y = x;
		while (y <= m and ID[q[y].l] == ID[q[x].l]) y++;

		//塊內暴力
		int right = len * ID[q[x].l] + len;
		//int right = len * ID[q[y].l]; 這樣不對,y不一定比x大
		
		while (x < y and q[x].r <= right - 1) {
			ll res = 0;
			for (int i = q[x].l; i <= q[x].r; i++) add(w[i], res);
			ans[q[x].id] = res;
			for (int i = q[x].l; i <= q[x].r; i++) cnt[w[i]]--;
			x++;
		}

		//塊外
		
		int l = right, r = right - 1;
		ll res = 0;
		while (x < y) {
			int ql = q[x].l, qr = q[x].r;
			while (r < qr)add(w[++r], res);
			ll _res = res;
			while (l > ql)add(w[--l], res);
			ans[q[x].id] = res;
			while (l < right) cnt[w[l++]] --;
			res = _res;
			x++;
		}
		memset(cnt, 0, sizeof cnt);
	}
	for (int i = 1; i <= m; i++)printf("%lld\n", ans[i]);
}

P5906 【模板】回滾莫隊&不刪除莫隊 - 洛谷

給定一個序列,多次詢問一段區間 \([l,r]\),求區間中相同的數的最遠間隔距離

序列中兩個元素的間隔距離指的是兩個元素下標差的絕對值

這個說是模板題,其實上一道題更模板。

#include<bits/stdc++.h>
using namespace std;

const int N = 2e5 + 10;
int w[N], ans[N], n, m, nums[N], ID[N], len;
int fir[N], last[N];
int mfir[N], mlast[N], mpos[N], mcnt, vis[N], vscnt;
struct Query {
	int id, l, r;
	bool operator < (const Query& b)const {
		if (ID[l] != ID[b.l])return ID[l] < ID[b.l];
		return r < b.r;
	}
}q[N];


void add(int pos, int val, int& res) {
	if (!fir[val]) fir[val] = pos;
	else fir[val] = min(fir[val], pos);

	if (!last[val])last[val] = pos;
	else last[val] = max(last[val], pos);

	res = max(res, last[val] - fir[val]);
}
int main() {
	scanf("%d", &n);
	int numID = 0;
	for (int i = 1; i <= n; i++)scanf("%d", w + i), nums[++numID] = w[i];

	sort(nums + 1, nums + 1 + n);
	numID = unique(nums + 1, nums + 1 + n) - nums - 1;

	for (int i = 1; i <= n; i++)w[i] = lower_bound(nums + 1, nums + 1 + numID, w[i]) - nums;

	len = sqrt(n);
	for (int i = 1; i <= n; i++)ID[i] = i / len;

	scanf("%d", &m);
	for (int i = 1; i <= m; i++) {
		scanf("%d%d", &q[i].l, &q[i].r); q[i].id = i;
	}

	sort(q + 1, q + 1 + m);

	for (int x = 1; x <= m;) {
		int y = x;
		while (y <= m and ID[q[y].l] == ID[q[x].l])y++;
		int right = ID[q[x].l] * len + len;

		while (x < y and q[x].r <= right - 1) {
			int res = 0, mcnt = 0; vscnt++;
			for (int i = q[x].l; i <= q[x].r; i++) {
				if (vis[w[i]] != vscnt) {
					vis[w[i]] = vscnt;
					mpos[++mcnt] = w[i];
					mfir[mcnt] = fir[w[i]];
					mlast[mcnt] = last[w[i]];
				}
				add(i, w[i], res);
			}
			ans[q[x].id] = res;
			for (int i = 1; i <= mcnt; i++) {
				fir[mpos[i]] = mfir[i];
				last[mpos[i]] = mlast[i];
			}
			x++;
		}

		int l = right, r = right - 1;
		int res = 0;

		while (x < y) {
			int ql = q[x].l, qr = q[x].r;
			while (r < qr)r++, add(r, w[r], res);
			int _res = res;
			mcnt = 0; vscnt++;
			while (l > ql) {
				l--;
				if (vis[w[l]] != vscnt) {
					vis[w[l]] = vscnt;
					mpos[++mcnt] = w[l];
					mfir[mcnt] = fir[w[l]];
					mlast[mcnt] = last[w[l]];
				}
				add(l, w[l], res);
			}
			ans[q[x].id] = res;
			for (int i = 1; i <= mcnt; i++) {
				fir[mpos[i]] = mfir[i];
				last[mpos[i]] = mlast[i];
			}
			l = right; res = _res;
			x++;
		}
		memset(fir, 0, sizeof fir); memset(last, 0, sizeof last);
	}
	for (int i = 1; i <= m; i++)printf("%d\n", ans[i]);
}

樹上莫隊

SP10707 COT2 - Count on a tree II - 洛谷

  • 給定 \(n\) 個結點的樹,每個結點有一種顏色。
  • \(m\) 次詢問,每次詢問給出 \(u,v\),回答 \(u,v\) 之間的路徑上的結點的不同顏色數。

很顯然,如果是在序列上的化就是最基礎的莫隊模板題

考慮轉化到序列上

尤拉序列\(DFS\)

可以解決這個問題

對於詢問 \((x,y)\)\(x\) 是在 \(DFS\) 中先出現的那個

分為兩種情況:

  • \(x == lca(x,y)\) , 則對應的區間是 \([in(x),in(y)]\) 中出現次數為一的點 ,如上圖紅色詢問
  • \(x \neq lca(x,y)\) , 則對應區間是 \([out(x), in(y)]\) 中出現次數為一的點,加上 \(lca\)

對於翻轉操作,可以用一個數組 \(st\) 進行標識,增添和刪除其實都是一樣的

/*
 * @Author: zhl
 * @Date: 2020-11-19 15:34:09
 */
#include<bits/stdc++.h>
#pragma GCC optimize(3)
#define pb(x) push_back(x)
using namespace std;

const int N = 1e5 + 10;
vector<int>G[N];

int n, m, len, ID[N], ans[N];
int w[N], nums[N], in[N], out[N], ord[N];
int tot, dep[N], f[N][32], cnt[N], st[N];

void add(int x, int& res) {
	st[x] ^= 1;
	if (!st[x]) {
		if (!--cnt[w[x]])res--;
	}
	else {
		if (!cnt[w[x]]++)res++;
	}
}


void dfs(int u, int p) {
	in[u] = ++tot;
	ord[tot] = u;

	dep[u] = dep[p] + 1; f[u][0] = p;
	for (int x = 1; (1 << x) < dep[u]; x++) {
		f[u][x] = f[f[u][x - 1]][x - 1];
	}
	for (int v : G[u]) {
		if (v == p)continue;
		dfs(v, u);
	}
	out[u] = ++tot;
	ord[tot] = u;
}

int LCA(int x, int y) {
	if (dep[x] < dep[y])swap(x, y);
	while (dep[x] != dep[y]) {
		int u = dep[x] - dep[y];
		int v = 0;
		while (!(u & (1 << v)))v++;
		x = f[x][v];
	}

	while (x != y) {
		int v = 0;
		while (f[x][v] != f[y][v])v++;
		x = f[x][max(0, v - 1)]; y = f[y][max(0, v - 1)];
	}
	return x;
}

struct Query {
	int id, l, r, lca;
	bool operator < (const Query& b)const {
		if (ID[l] != ID[b.l])return ID[l] < ID[b.l];
		return r < b.r;
	}
}q[N];

int main() {
	scanf("%d%d", &n, &m); int numID = 0;
	for (int i = 1; i <= n; i++)scanf("%d", w + i), nums[++numID] = w[i];

	sort(nums + 1, nums + 1 + n);
	numID = unique(nums + 1, nums + 1 + n) - nums - 1;

	for (int i = 1; i <= n; i++)w[i] = lower_bound(nums + 1, nums + 1 + numID, w[i]) - nums;

	for (int i = 1; i < n; i++) {
		int a, b; scanf("%d%d", &a, &b);
		G[a].pb(b); G[b].pb(a);
	}

	dfs(1, 0);
	len = sqrt(tot);
	for (int i = 1; i <= tot; i++)ID[i] = i / len;

	for (int i = 1; i <= m; i++) {
		int x, y; scanf("%d%d", &x, &y);
		int lca = LCA(x, y);
		if (in[x] > in[y])swap(x, y);

		if (lca == x) {
			q[i] = { i,in[x],in[y],0 };
		}
		else {
			q[i] = { i,out[x],in[y],lca };
		}
	}
	sort(q + 1, q + 1 + m);

	int res = 0, l = 1, r = 0;
	for (int i = 1; i <= m; i++) {
		int ql = q[i].l, qr = q[i].r, lca = q[i].lca;

		if (lca != 0)add(lca, res);
		while (l < ql)add(ord[l++], res);
		while (l > ql)add(ord[--l], res);
		while (r < qr)add(ord[++r], res);
		while (r > qr)add(ord[r--], res);

		ans[q[i].id] = res;
		if (lca != 0)add(lca, res);
	}
	for (int i = 1; i <= m; i++)printf("%d\n", ans[i]);
}

二次離線莫隊

P4887 【模板】莫隊二次離線(第十四分塊(前體)) - 洛谷

給一個序列 \(a\) ,每次給一個查詢區間 \([l,r]\)

查詢 \(l \le i < j \le r\)\(a_i\) 異或 \(a_j\) 正好有 \(k\) 個二進位制 \(bit\) 的個數

還是用莫隊的思想,在挪的時候更新答案的方式如圖

現在我們要解決的問題是如何求 \(A_i\) 與區間 \([L,R]\) 的配對數。

這裡我們利用字首和的思想,令 \(S_R\) 表示區間 \([1,R]\)\(A_i\) 的配對數

則要求的就是 \(S_R - S_{L-1}\)

兩個數配對指的是異或滿足題目

我們可以設 \(f[i]\) 表示 \([1,i]\)\(A_{i+1}\) 的配配對數目所以對於上圖的情況有一個\(S\) 可以用 \(f\) 來表示,還有一個就再次離線,到對應的點上。

求出 \(f\) 陣列

vector<int>nums;
for (int i = 0; i < (1 << 14); i++)
	if (count(i) == k)nums.push_back(i);

for (int i = 1; i <= n; i++) {
	for (int v : nums)g[w[i] ^ v]++;
	f[i] = g[w[i + 1]];
}

舉例當 \(r < qr\)

if (r < qr)subs[l - 1].push_back({ i, r + 1, qr, -1 });
while (r < qr) q[i].res += f[r++];

每一次挪動都要加上 \(f[r]\) 還要減去區間 \([1,l-1]\)\(A_{r+1},A_{r+2},...,A_{qr}\) 的配對數目,對於這個東西,可以把它掛到 \(l-1\) 上, 這個掛挺形象的。

後面挪 \(l\) 的時候要注意自己跟自己匹配的情況,只有 \(k == 0\) 的時候自己可以匹配自己。

注意自己是不能匹配自己的,但是這種情況在計算 \(S\) 的時候是被計算在內的。

最後處理離線查詢

memset(g, 0, sizeof g);
for (int i = 1; i <= n; i++) {
	for (int v : nums)g[v ^ w[i]]++;
	for (auto& x : subs[i]) {
		for (int i = x.l; i <= x.r; i++) {
			q[x.id].res += x.t * g[w[i]];
		}
	}
}

這一塊的複雜度是 \(r\) 的移動次數 \(O(n \sqrt n)\) 量級

/*
 * @Author: zhl
 * @Date: 2020-11-19 20:16:41
 */
#include<bits/stdc++.h>
using namespace std;

typedef long long ll;
const int N = 1e5 + 10;

int n, m, k, len, ID[N], f[N], g[N], w[N];
ll ans[N];
struct Query {
	int id, l, r;
	ll res;
	bool operator < (const Query& b)const {
		if (ID[l] != ID[b.l]) return ID[l] < ID[b.l];
		return r < b.r;
	}
}q[N];

struct subquery {
	int id, l, r, t;
};
vector<subquery>subs[N];

int count(int n) {
	int ans = 0;
	while (n) ans++, n -= -n & n;
	return ans;
}
int main() {
	scanf("%d%d%d", &n, &m, &k);
	for (int i = 1; i <= n; i++)scanf("%d", w + i);

	len = sqrt(n);
	for (int i = 1; i <= n; i++)ID[i] = i / len;

	vector<int>nums;
	for (int i = 0; i < (1 << 14); i++)if (count(i) == k)nums.push_back(i);

	for (int i = 1; i <= n; i++) {
		for (int v : nums)g[w[i] ^ v]++;
		f[i] = g[w[i + 1]];
	}

	for (int i = 1; i <= m; i++) {
		scanf("%d%d", &q[i].l, &q[i].r); q[i].id = i;
	}

	sort(q + 1, q + 1 + m);

	int l = 1, r = 0;
	for (int i = 1; i <= m; i++) {
		int ql = q[i].l, qr = q[i].r;
		//在 l - 1 處 掛上一個離線查詢
		if (r < qr)subs[l - 1].push_back({ i, r + 1, qr, -1 });
		while (r < qr) q[i].res += f[r++];

		if (r > qr)subs[l - 1].push_back({ i, qr + 1,r ,1 });
		while (r > qr) q[i].res -= f[--r];

		if (l < ql)subs[r].push_back({ i, l, ql - 1, -1 });
		while (l < ql) q[i].res += f[l - 1] + !k, l++;

		if (l > ql)subs[r].push_back({ i, ql, l - 1,1 });
		while (l > ql) q[i].res -= f[l - 2] + !k, l--;
	}
	memset(g, 0, sizeof g);
	for (int i = 1; i <= n; i++) {
		for (int v : nums)g[v ^ w[i]]++;
		for (auto& x : subs[i]) {
			for (int i = x.l; i <= x.r; i++) {
				q[x.id].res += x.t * g[w[i]];
			}
		}
	}
	for (int i = 2; i <= m; i++) q[i].res += q[i - 1].res;
	for (int i = 1; i <= m; i++)ans[q[i].id] = q[i].res;
	for (int i = 1; i <= m; i++)printf("%lld\n", ans[i]);
}

6.9 樹套樹

一種思想,就是一棵樹的節點是另一顆樹。

在外面的叫外層樹,在裡面的叫內層樹。

外層樹一般是, 樹狀陣列, 線段樹

內層樹一般是 平衡樹 , STL , 線段樹

線段樹套STL

/*
 * @Author: zhl
 * @Date: 2020-11-16 12:50:32
 */
#include<bits/stdc++.h>
#define lo (o<<1)
#define ro (o<<1|1)
#define mid (l+r>>1)
using namespace std;

const int N = 5e4 + 10, inf = 1e9;

multiset<int>s[N << 2];
int A[N];
void build(int o, int l, int r) {
	s[o].insert(inf); s[o].insert(-inf);
	for (int i = l; i <= r; i++) s[o].insert(A[i]);
	if (l == r)return;
	build(lo, l, mid);
	build(ro, mid + 1, r);
}
void updt(int o, int l, int r, int pos, int v) {
	s[o].erase(s[o].lower_bound(A[pos]));
	s[o].insert(v);
	if (l == r)return;
	if (pos <= mid) updt(lo, l, mid, pos, v);
	else updt(ro, mid + 1, r, pos, v);
}

int query(int o, int l, int r, int L, int R, int v) {
	if (L <= l and r <= R) return *prev(s[o].lower_bound(v));
	int ans = -inf;
	if (L <= mid)ans = max(ans, query(lo, l, mid, L, R, v));
	if (R > mid) ans = max(ans, query(ro, mid + 1, r, L, R, v));
	return ans;
}
int n, m;
int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++)scanf("%d", A + i);
	build(1, 1, n);
	while (m--) {
		int op, a, b, x; scanf("%d", &op);
		if (op == 1) {
			scanf("%d%d", &a, &b);
			updt(1, 1, n, a, b);
			A[a] = b;
		}
		else {
			scanf("%d%d%d", &a, &b, &x);
			printf("%d\n", query(1, 1, n, a, b, x));
		}
	}
}

線段樹套平衡樹

【樹套樹模板】二逼平衡樹

很多棵樹的時候可以開一個 root 陣列就可以,這樣可以不需要傳引用,因為在splay的時候會更新 root陣列

rotate 不可以任意順序,會有影響

/*
 * @Author: zhl
 * @Date: 2020-11-16 13:51:18
 */

#include<bits/stdc++.h>
#define mid (l+r>>1)
#define lo (o<<1)
#define ro (o<<1|1)
using namespace std;

const int N = 2e6 + 10, inf = 0x7fffffff;

struct node {
	int s[2], size, p, v;
	void init(int _p, int _v) {
		p = _p; v = _v; size = 1;
	}
}tr[N];

int w[N], n, m, root[N], idx;

void push_up(int u) {
	tr[u].size = tr[tr[u].s[0]].size + tr[tr[u].s[1]].size + 1;
}
void rotate(int x) {
	int y = tr[x].p, z = tr[y].p;
	int k = tr[y].s[1] == x;
	tr[z].s[tr[z].s[1] == y] = x; tr[x].p = z;
	tr[y].s[k] = tr[x].s[k ^ 1]; tr[tr[x].s[k ^ 1]].p = y; //草這兩行順序不能換
	tr[x].s[k ^ 1] = y; tr[y].p = x;
	push_up(y), push_up(x);
}

void splay(int x, int k,int rt) {
	
	while (tr[x].p != k) {
		int y = tr[x].p, z = tr[y].p;
		if (z != k) {
			if ((tr[z].s[0] == y) ^ (tr[y].s[0] == x)) rotate(x);
			else rotate(y);
		}
		rotate(x);
	}
	if (!k)root[rt] = x;
}

void insert(int v, int rt) {
	int u = root[rt], p = 0;
	while (u) p = u, u = tr[u].s[v > tr[u].v];
	u = ++idx;
	if (p)tr[p].s[v > tr[p].v] = u;
	tr[u].init(p, v);
	splay(u, 0, rt);
}

int get_rank(int v, int rt) {
	int u = root[rt], res = 0;
	while (u) {
		if (v > tr[u].v) res += tr[tr[u].s[0]].size + 1, u = tr[u].s[1];
		else u = tr[u].s[0];
	}
	return res;
}
void build(int o, int l, int r) {
	insert(-inf, o); insert(inf, o);
	for (int i = l; i <= r; i++) {
		insert(w[i], o);
	}
	if (l == r)return;
	build(lo, l, mid);
	build(ro, mid + 1, r);
}

int query_rank(int o, int l, int r, int L, int R, int x) {
	if (L <= l and r <= R)return get_rank(x, o) - 1;
	int ans = 0;
	if (L <= mid)ans += query_rank(lo, l, mid, L, R, x);
	if (R > mid) ans += query_rank(ro, mid + 1, r, L, R, x);
	return ans;
}
void updt(int o, int l, int r, int pos, int v){
	int u = root[o];
	while (u) {
		if (tr[u].v == w[pos])break;
		if (w[pos] > tr[u].v)u = tr[u].s[1];
		if (w[pos] < tr[u].v) u = tr[u].s[0];
	}
	splay(u, 0, o);
	int ls = tr[u].s[0], rs = tr[u].s[1];
	while (tr[ls].s[1]) ls = tr[ls].s[1];
	while (tr[rs].s[0]) rs = tr[rs].s[0];
	splay(ls, 0, o); splay(rs, ls, o);
	tr[rs].s[0] = 0;
	push_up(rs); push_up(ls);
	insert(v, o);
	if (l == r)return; //不要忘記結束條件
	if (pos <= mid) {
		updt(lo, l, mid, pos, v);
	}
	else {
		updt(ro, mid + 1, r, pos, v);
	}
}
int get_pre(int x,int rt) {
	int u = root[rt], res = -inf;
	while (u) {
		if (tr[u].v >= x) u = tr[u].s[0];
		else res = tr[u].v, u = tr[u].s[1];
	}
	return res;
}
int get_suc(int x,int rt) {
	int u = root[rt], res = -inf;
	while (u) {
		if (tr[u].v <= x) u = tr[u].s[1];
		else res = tr[u].v, u = tr[u].s[0];
	}
	return res;
}

int query_pre(int o, int l, int r, int L, int R, int x) {
	if (L <= l and r <= R)return get_pre(x, o);
	int ans = -inf;
	if (L <= mid)ans = max(ans, query_pre(lo, l, mid, L, R, x));
	if (R > mid) ans = max(ans, query_pre(ro, mid + 1, r, L, R, x));
	return ans;
}

int query_suc(int o, int l, int r, int L, int R, int x) {
	if (L <= l and r <= R)return get_suc(x, o);
	int ans = inf;
	if (L <= mid)ans = min(ans, query_suc(lo, l, mid, L, R, x));
	if (R > mid) ans = min(ans, query_suc(ro, mid + 1, r, L, R, x));
	return ans;
}
int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++)scanf("%d", w + i);
	build(1, 1, n);
	while (m--) {
		int op, a, b, k, pos;
		scanf("%d", &op);
		if (op == 1) {
			scanf("%d%d%d", &a, &b, &k);
			printf("%d\n", query_rank(1, 1, n, a, b, k) + 1);
		}
		else if (op == 2) {
			scanf("%d%d%d", &a, &b, &k);
			int l = 0, r = 1e8;
			while (l < r) {
				int m = l + r + 1 >> 1;
				if (query_rank(1, 1, n, a, b, m) + 1 <= k) {
					l = m;
				}
				else {
					r = m - 1;
				}
			}
			printf("%d\n", r);
		}
		else if (op == 3) {
			scanf("%d%d", &pos, &k);
			updt(1, 1, n, pos, k);
			w[pos] = k;
		}
		else if (op == 4) {
			scanf("%d%d%d", &a, &b, &k);
			printf("%d\n", query_pre(1, 1, n, a, b, k));
		}
		else if (op == 5) {
			scanf("%d%d%d", &a, &b, &k);
			printf("%d\n", query_suc(1, 1, n, a, b, k));
		}
	}

}

權值線段樹套線段樹

之前沒有做過權值線段樹套外層樹,也是第一次寫動態開點的完整線段樹

push_down 的時候要動態開點。其實跟普通線段樹是一樣的。

寫之前一定要先想清楚。外層的線段樹是將值離散化後建的,外層的線段樹可以用 o<<1o<<1|1 , 這樣進行轉移。內層的線段樹需要動態開點,所以需要陣列 lc[N] , rc[N]

要計算開的空間大小。

/*
 * @Author: zhl
 * @Date: 2020-11-17 13:55:59
 */
#include<bits/stdc++.h>
#define mid (l + r >> 1)
#define lo (o << 1)
#define ro (o << 1 | 1)
using ll = long long;
using namespace std;

const int N = 5e4 + 10, P = N * 17 * 17, M = 4 * N;
int n, m;

int root[M], lc[P], rc[P], lz[P], tot;
ll sum[P];

void push_down(int u,int l,int r) {
	if (!lc[u])lc[u] = ++tot;
	lz[lc[u]] += lz[u];
	if (!rc[u])rc[u] = ++tot;
	lz[rc[u]] += lz[u];

	sum[lc[u]] += (mid - l + 1) * lz[u];
	sum[rc[u]] += (r - mid) * lz[u];

	lz[u] = 0;
}
void updt(int& rt, int L,int R, int o = 1, int l = 1, int r = n) {
	if (!rt) rt = ++tot;
	if (L <= l and r <= R) {
		lz[rt] ++;
		//sum[rt] += R - L + 1;//我是傻逼草
		sum[rt] += r - l + 1;
		return;
	}
	if (lz[rt])push_down(rt, l , r);
	if (L <= mid)updt(lc[rt], L, R, lo, l, mid);
	if (R > mid)updt(rc[rt], L, R, ro, mid + 1, r);
	sum[rt] = sum[lc[rt]] + sum[rc[rt]];
}

ll query(int rt, int L, int R, int o = 1, int l = 1, int r = n) {
	if (!rt)return 0;
	if (L <= l and r <= R) {
		return sum[rt];
	}
	if (lz[rt])push_down(rt, l, r);
	ll ans = 0;
	if (L <= mid)ans += query(lc[rt], L, R, lo, l, mid);
	if (R > mid) ans += query(rc[rt], L, R, ro, mid + 1, r);
	return ans;
}


int cntID, id[N];

void add(int L,int R, int pos, int o = 1, int l = 1, int r = cntID) {
	updt(root[o], L, R);
	if (l == r)return;
	if (pos <= mid)add(L, R, pos, lo, l, mid);
	else add(L, R, pos, ro, mid + 1, r);
}

int get_kth(int L,int R,ll k,int o = 1, int l = 1, int r = cntID) {
	if (l == r)return id[l];
	
	ll num = query(root[ro], L, R);
	if (num >= k) return get_kth(L, R, k, ro, mid + 1, r);
	else return get_kth(L, R, k - num, lo, l, mid);
}


struct {
	int op, a, b, c;
}q[N];

int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= m; i++) {
		scanf("%d%d%d%d", &q[i].op, &q[i].a, &q[i].b, &q[i].c);
		if (q[i].op == 1) {
			id[++cntID] = q[i].c;
		}
	}
	sort(id + 1, id + 1 + cntID);
	cntID = unique(id + 1, id + 1 + cntID) - id - 1;

	for (int i = 1; i <= m; i++) {
		if (q[i].op == 1)q[i].c = lower_bound(id + 1, id + 1 + cntID, q[i].c) - id;
	}

	for (int i = 1; i <= m; i++) {
		if (q[i].op == 1) {
			add(q[i].a, q[i].b, q[i].c);
		}
		else {
			printf("%d\n", get_kth(q[i].a, q[i].b, q[i].c));
		}
	}
}

6.10 LCT

\(LCT\) 可以動態維護一個森林

每個節點最多隻能連線一條實邊 ,被父親節點指向的實邊不屬於自己的實邊。

實邊和虛邊是維護的一種方式,實邊和虛邊在原圖中都是真實存在的邊。

一棵樹中的實邊和虛邊可以相互變換

\(Splay\) 維護所有的實邊

基本操作

Access(x)

\(x\) 所在的樹的根節點與 \(x\) 之間的路徑變成實邊路徑

並且將 \(x\) 旋轉至該實邊路徑 \(Splay\) 的根節點

不改變原樹的結構

make_root(x)

\(x\) 變成根節點,這個根節點指的是原樹的根節點

find_root(x)

找到 \(x\) 所在樹的根節點

將根節點與 \(x\) 之間的路徑變成實邊路徑,並且將 原樹的根節點 旋轉到 \(Splay\) 的根節點

split(x,y)

\(x\)\(y\) 建立一條實邊路徑

先連通根節點和 \(x\) 成為實體路徑,然後將 \(x\) 變成根

然後連通根節點 \(x\)\(y\)

link(x,y)

\(x\) , \(y\) 不連通,則建立一條虛邊

cut(x,y)

\(x\) , \(y\) 存在邊,則刪除它

/*
 * @Author: zhl
 * @Date: 2020-11-20 15:08:50
 */

#include<bits/stdc++.h>
using namespace std;

const int N = 1e5 + 10;

int n, m;

struct node {
	int s[2], p, v;
	int sum, rev;
}tr[N];
int stk[N];


void rev(int x) {
	swap(tr[x].s[0], tr[x].s[1]);
	tr[x].rev ^= 1;
}

void push_up(int x) {
	tr[x].sum = tr[tr[x].s[0]].sum ^ tr[tr[x].s[1]].sum ^ tr[x].v;
}

void push_down(int x) {
	if (tr[x].rev) {
		rev(tr[x].s[0]); rev(tr[x].s[1]);
		tr[x].rev = 0;
	}
}

bool is_root(int x) {
	return tr[tr[x].p].s[0] != x and tr[tr[x].p].s[1] != x;
}

void rotate(int x) {
	int y = tr[x].p, z = tr[y].p;
	int k = tr[y].s[1] == x;
	if (!is_root(y)) tr[z].s[tr[z].s[1] == y] = x;
	tr[x].p = z;
	tr[y].s[k] = tr[x].s[k ^ 1], tr[tr[x].s[k ^ 1]].p = y;
	tr[x].s[k ^ 1] = y; tr[y].p = x;
	push_up(y), push_up(x);
}

void splay(int x) {
	int top = 0, r = x;
	stk[++top] = r;
	while (!is_root(r)) stk[++top] = r = tr[r].p;
	while (top)push_down(stk[top--]); 

	while (!is_root(x)) {
		int y = tr[x].p, z = tr[y].p;
		if (!is_root(y)){
			if((tr[y].s[0] == x) ^ (tr[z].s[0] == y))rotate(x);
			else rotate(y);
		}
		rotate(x);
	}
}

void access(int x) {
	/*
		將 x 所在的樹的根節點與 x 之間的路徑變成實邊路徑
		並且將 x 旋轉至該實邊路徑splay的根節點
	*/
	int z = x;
	for (int y = 0; x; y = x, x = tr[x].p) {
		splay(x);
		tr[x].s[1] = y, push_up(x);
	}
	splay(z);
}

void make_root(int x) {
	/*
		將 x 變成 x 所在樹中的根節點
	*/
	access(x);
	rev(x);
}

int find_root(int x) {
	/*
		查詢 x 所在樹的根節點
		將根節點與 x 之間的路徑變成實邊路徑,並且將 原樹的根節點 旋轉到splay的根節點
	*/
	access(x);
	while (tr[x].s[0])push_down(x), x = tr[x].s[0];
	splay(x);
	return x;      
}

void split(int x, int y) {
	/*
		將 x 和 y 建立一條實邊路徑
		先連通根節點和 x 成為實體路徑,然後將 x 變成根
		然後連通根節點 x 和 y
	*/
	make_root(x);
	access(y);
}

void link(int x, int y) {
	/*
		若 x 和 y 不連通
		建立一虛邊
	*/
	make_root(x);
	if (find_root(y) != x) tr[x].p = y;
}

void cut(int x, int y) {
	/*
		若 x, y 存在邊
		則刪除它
	*/
	make_root(x);
	if (find_root(y) == x and tr[y].p == x and !tr[y].s[0]) {
		tr[x].s[1] = tr[y].p = 0;
		push_up(x);
	}
}

int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++)scanf("%d", &tr[i].v);

	while (m--) {
		int t, x, y;
		scanf("%d%d%d", &t, &x, &y);
		if (t == 0) {
			split(x, y);
			printf("%d\n", tr[y].sum);
		}
		else if (t == 1) link(x, y);
		else if (t == 2) cut(x, y);
		else {
			splay(x);
			tr[x].v = y;
			push_up(x);
		}
	}
}

魔法森林

求路徑上兩種屬性最大值之和的最小值

/*
 * @Author: zhl
 * @Date: 2020-11-20 15:08:42
 */
#include<bits/stdc++.h>
#pragma GCC optimize(2)
using namespace std;

const int N = 150010, INF = 1e9;

struct Edge {
	int x, y, a, b;
	bool operator < (const Edge& rhs)const {
		return a < rhs.a;
	}
}E[N];

struct node {
	int s[2], p, v;
	int mx; //最大值的下標
	int rev;
}tr[N];

int n, m, fa[N];
int find(int a) { return a == fa[a] ? a : fa[a] = find(fa[a]); }

int stk[N];
void rev(int x) {
	swap(tr[x].s[0], tr[x].s[1]);
	tr[x].rev ^= 1;
}
void push_up(int x) {
	tr[x].mx = x;
	for (int i = 0; i < 2; i++) {
		if (tr[tr[tr[x].s[i]].mx].v > tr[tr[x].mx].v) {
			tr[x].mx = tr[tr[x].s[i]].mx;
		}
	}
}
void push_down(int x) {
	if (tr[x].rev) {
		rev(tr[x].s[0]), rev(tr[x].s[1]);
		tr[x].rev = 0;
	}
}

bool is_root(int x) {
	return tr[tr[x].p].s[0] != x and tr[tr[x].p].s[1] != x;
}

void rotate(int x) {
	int y = tr[x].p, z = tr[y].p;
	int k = tr[y].s[1] == x;
	if (!is_root(y)) tr[z].s[tr[z].s[1] == y] = x;
	tr[x].p = z;
	tr[y].s[k] = tr[x].s[k ^ 1], tr[tr[x].s[k ^ 1]].p = y;
	tr[x].s[k ^ 1] = y; tr[y].p = x;
	push_up(y), push_up(x);
}

void splay(int x) {
	int top = 0, r = x;
	stk[++top] = r;
	while (!is_root(r)) stk[++top] = r = tr[r].p;
	while (top)push_down(stk[top--]);

	while (!is_root(x)) {
		int y = tr[x].p, z = tr[y].p;
		if (!is_root(y)) {
			if ((tr[y].s[0] == x) ^ (tr[z].s[0] == y))rotate(x);
			else rotate(y);
		}
		rotate(x);
	}
}
void access(int x) {
	int z = x;
	for (int y = 0; x; y = x, x = tr[x].p) {
		splay(x);
		tr[x].s[1] = y, push_up(x);
	}
	splay(z);
}

void make_root(int x) {
	access(x);
	rev(x);
}

int find_root(int x) {
	access(x);
	while (tr[x].s[0])push_down(x), x = tr[x].s[0];
	splay(x);
	return x;
}

void split(int x, int y) {
	make_root(x);
	access(y);
}

void link(int x, int y) {
	make_root(x);
	if (find_root(y) != x) tr[x].p = y;
}

void cut(int x, int y) {
	make_root(x);
	if (find_root(y) == x and tr[y].p == x and !tr[y].s[0]) {
		tr[x].s[1] = tr[y].p = 0;
		push_up(x);
	}
}


int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= m; i++) {
		scanf("%d%d%d%d", &E[i].x, &E[i].y, &E[i].a, &E[i].b);
	}
	sort(E + 1, E + 1 + m);

	for (int i = 1; i <= n + m; i++) {
		fa[i] = i;
		if (i > n) tr[i].v = E[i - n].b;
		tr[i].mx = i;
	}
	int res = INF;

	for (int i = 1; i <= m; i++) {
		int x = E[i].x, y = E[i].y, a = E[i].a, b = E[i].b;

		if (find(x) == find(y)) {
			split(x, y);
			int t = tr[y].mx;
			if (tr[t].v > b) {
				cut(E[t - n].x, t); cut(t, E[t - n].y);
				link(x, n + i); link(n + i, y);
			}
		}
		else {
			fa[find(x)] = find(y);
			link(x, n + i); link(n + i, y);
		}
		if (find(1) == find(n)) {
			split(1, n);
			res = min(res, a + tr[tr[n].mx].v);
		}
	}
	if (res == INF)puts("-1");
	else printf("%d\n", res);
}

6.11 左偏樹

說是左偏樹,其實叫可並堆更合理

對一個節點來說,它的左右子樹的值都要大於它自己的值

定義距離 \(dis\) : 每個節點到空節點的距離

左偏樹有一個重要的性質:

  • 每個子樹的左兒子的 \(dis\) 要大於等於 右兒子的 \(dis\) ,所以直覺上來說就是

“左偏”

  • 插入 : \(O(log n)\)
  • 求最小值: \(O(1)\)
  • 刪除最小值: \(O(log n)\)
  • 合併兩棵樹\(O(log n)\)

P3377 【模板】左偏樹(可並堆) - 洛谷

/*
 * @Author: zhl
 * @Date: 2020-11-21 11:16:27
 */
#include<bits/stdc++.h>
using namespace std;

const int N = 2e5 + 10;

int l[N], r[N], v[N], fa[N], idx, dis[N];

int find(int a) {
	return a == fa[a] ? a : fa[a] = find(fa[a]);
}
bool cmp(int a, int b) {
	if (v[a] != v[b])return v[a] < v[b];
	return a < b;
}
int merge(int a, int b) {
	if (!a or !b)return a + b;
	if (cmp(b, a))swap(a, b);
	r[a] = merge(r[a], b);
	if (dis[r[a]] > dis[l[a]])swap(l[a], r[a]);
	dis[a] = dis[r[a]] + 1;
	return a;
}
int n;
int main() {
	v[0] = 2e9;
	scanf("%d", &n);
	while (n--) {
		int op, a, b;
		scanf("%d%d", &op, &a);
		if (op == 1) {
			v[++idx] = a;
			dis[idx] = 1;
			fa[idx] = idx;
		}
		else if (op == 2) {
			scanf("%d", &b);
			a = find(a), b = find(b);
			if (a != b) {
				if (cmp(b, a)) swap(a, b);
				fa[b] = a;
				merge(a, b);
			}
		}
		else if (op == 3) {
			printf("%d\n", v[find(a)]);
		}
		else {
			a = find(a);
			if (cmp(r[a], l[a]))swap(l[a], r[a]);
			fa[a] = l[a]; fa[l[a]] = l[a];
			merge(l[a], r[a]);
		}
	}
}

6.12 ST表

/*
 * @Author: zhl
 * @LastEditTime: 2020-12-09 11:02:17
 */

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;

int f[N][32];
int A[N];

int n, m, x, y;
void init() {
	for (int i = 1; i <= n; i++) {
		f[i][0] = A[i];
	}
	for (int j = 1; (1 << j) <= n; j++) {
		for (int i = 1; i + (1 << j) - 1 <= n; i++) {
			f[i][j] = max(f[i][j - 1], f[i + (1 << (j-1))][j - 1]);
		}
	}
}

int query(int l, int r) {
	int k = log2(r - l + 1);
	return max(f[l][k], f[r - (1 << k) + 1][k]);
}
int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++) {
		scanf("%d", A + i);
	}
	init();
	for (int i = 1; i <= m; i++) {
		scanf("%d%d", &x, &y);
		printf("%d\n", query(x, y));
	}
}

6.13 樹鏈剖分

/*
 * @Author: zhl
 * @Date: 2020-10-13 20:36:59
 */
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i = a;i <= b;i++)
#define repE(i,u) for(int i = head[u];i;i = E[i].next)
#define mid (l+r>>1)
#define lo (o<<1)
#define ro (o<<1|1)

using namespace std;
const int N = 4e5 + 10;
int A[N];
int n, m, root, mod;

struct Edge {
	int to, next;
}E[N << 1];

int head[N], tot;
void addEdge(int from, int to) {
	E[++tot] = Edge{ to,head[from] };
	head[from] = tot++;
}

int fa[N], sz[N], Tfa[N], dep[N], son[N];

//dfs1處理dep,sz,fa,son(重兒子)
void dfs1(int u, int p) {
	fa[u] = p;
	dep[u] = dep[p] + 1;
	sz[u] = 1;
	int mx = -1;
	repE(i, u) {
		int v = E[i].to;
		if (v == p)continue;
		dfs1(v, u);
		sz[u] += sz[v];
		if (sz[v] > mx)mx = sz[v], son[u] = v;
	}
}

int cnt;
int id[N], val[N], top[N];
//dfs2 剖分數鏈
void dfs2(int u, int topf) {
	id[u] = ++cnt;
	val[cnt] = A[u];
	top[u] = topf;
	if (!son[u])return;
	dfs2(son[u], topf);
	repE(i, u) {
		int v = E[i].to;
		if (v == fa[u] or v == son[u])continue;
		dfs2(v, v);
	}
}

int sum[N << 2], lz[N << 2];
int x, y, z;
void push_down(int o, int l, int r) {
	if (!lz[o])return;
	lz[lo] += lz[o];
	lz[ro] += lz[o];
	sum[lo] = (sum[lo] + lz[o] * (mid - l + 1)) % mod;
	sum[ro] = (sum[ro] + lz[o] * (r - mid)) % mod;
	lz[o] = 0;
}

void build(int o, int l, int r) {
	if (l == r) {
		sum[o] = val[l];
		return;
	}
	build(lo, l, mid);
	build(ro, mid + 1, r);
	sum[o] += sum[lo] + sum[ro];
}

void updt(int o, int l, int r) {
	if (x <= l and r <= y) {
		lz[o] = (lz[o] + z) % mod;
		sum[o] = (sum[o] + z * (r - l + 1)) % mod;
		return;
	}
	push_down(o, l, r);
	if (x <= mid)updt(lo, l, mid);
	if (y > mid)updt(ro, mid + 1, r);
	sum[o] = (sum[lo] + sum[ro]) % mod;
}

int query(int o, int l, int r) {
	if (x <= l and r <= y) {
		return sum[o];
	}
	int ans = 0;
	push_down(o, l, r);
	if (x <= mid)ans = (ans + query(lo, l, mid)) % mod;
	if (y > mid)ans = (ans + query(ro, mid + 1, r)) % mod;
	return ans;
}

int query_path(int a, int b) {
	int ans = 0;
	while (top[a] != top[b]) {
		if (dep[top[a]] < dep[top[b]])swap(a, b);
		x = id[top[a]]; y = id[a];
		ans = (ans + query(1, 1, cnt)) % mod;
		a = fa[top[a]];
	}
	if (dep[a] > dep[b])swap(a, b);
	x = id[a]; y = id[b];
	ans = (ans + query(1, 1, cnt)) % mod;
	return ans;
}
void updt_path(int a, int b, int k) {
	k %= mod;
	while (top[a] != top[b]) {
		if (dep[top[a]] < dep[top[b]])swap(a, b);
		x = id[top[a]]; y = id[a]; z = k;
		updt(1, 1, cnt);
		a = fa[top[a]];
	}
	if (dep[a] > dep[b])swap(a, b);
	x = id[a]; y = id[b]; z = k;
	updt(1, 1, cnt);
}

int main() {
	scanf("%d%d%d%d", &n, &m, &root, &mod);
	for (int i = 1; i <= n; i++)scanf("%d", A + i);

	for (int i = 1; i < n; i++) {
		scanf("%d%d", &x, &y);
		addEdge(x, y); addEdge(y, x);
	}
	dfs1(root, 0);
	dfs2(root, root);
	build(1, 1, cnt);
	while (m--) {
		int op; scanf("%d", &op);
		int a, b, c;
		if (op == 1) {//a,b 路徑 + c
			scanf("%d%d%d", &a, &b, &c);
			updt_path(a, b, c);
		}
		if (op == 2) {//a,b 路徑sum
			scanf("%d%d", &a, &b);
			printf("%d\n", query_path(a, b));
		}
		if (op == 3) {//a的subtree + c
			scanf("%d%d", &a, &c);
			x = id[a]; y = id[a] + sz[a] - 1;
			z = c;
			updt(1, 1, cnt);
		}
		if (op == 4) {
			scanf("%d", &a);
			x = id[a]; y = id[a] + sz[a] - 1;
			printf("%d\n", query(1, 1, cnt));
		}
	}
}

/*
5 50 2 24000
7 3 7 8 0
1 2
1 5
3 1
4 1
*/

6.14 LCA

倍增法

#include<bits/stdc++.h>
#define repE(i,u) for(int i = head[u];i;i = E[i].next)
using namespace std;
const int N = 1e6 + 10;

int f[N][32];
int dep[N];
struct Edge {
	int to, next;
}E[N << 1];

int head[N], tot;
void addEdge(int from, int to) {
	E[++tot] = Edge{ to,head[from] };
	head[from] = tot++;
}

void init(int u, int p) {
	dep[u] = dep[p] + 1;
	f[u][0] = p;
	for (int x = 1; (1 << x) < dep[u]; x++) {
		f[u][x] = f[f[u][x - 1]][x - 1];
	}

	repE(i, u) {
		if (E[i].to == p)continue;
		init(E[i].to, u);
	}
}

int LCA(int x, int y) {
	if (dep[x] < dep[y])swap(x, y);
	while (dep[x] != dep[y]) {
		int u = dep[x] - dep[y];
		int v = 0;
		while (!(u & (1 << v)))v++;
		x = f[x][v];
	}

	while (x != y) {
		int v = 0;
		while (f[x][v] != f[y][v])v++;
		x = f[x][max(0,v - 1)]; y = f[y][max(0,v - 1)];
	}
	return x;
}

int n, m, root;
int main() {
	scanf("%d%d%d", &n, &m, &root);
	for (int i = 1; i < n; i++) {
		int x, y; scanf("%d%d", &x, &y);
		addEdge(x, y);
		addEdge(y, x);
	}
	init(root, 0);
	for (int i = 1; i <= m; i++) {
		int x, y; scanf("%d%d", &x, &y);
		printf("%d\n", LCA(x, y));
	}
}

樹鏈剖分

#include<bits/stdc++.h>
using namespace std;

const int N = 5e5 + 10;
struct Edge {
	int to, next;
}E[N << 1];


int head[N], tot;
void addEdge(int from, int to) {
	E[++tot] = Edge{ to,head[from] };
	head[from] = tot;
}

int n, m, root;
int sz[N], son[N], top[N], dep[N], fa[N];
void dfs1(int u, int p) {
	fa[u] = p; sz[u] = 1; dep[u] = dep[p] + 1;
	for (int i = head[u]; i; i = E[i].next) {
		int v = E[i].to;
		if (v == p)continue;
		dfs1(v, u);
		sz[u] += sz[v];
		if (sz[v] > sz[son[u]])son[u] = v;
	}
}
void dfs2(int u, int tp) {
	top[u] = tp;
	if (son[u])dfs2(son[u], tp); else return;
	for (int i = head[u]; i;i = E[i].next) {
		int v = E[i].to;
		if (v == fa[u] or v == son[u])continue;
		dfs2(v, v);
	}
}
int lca(int a, int b) {
	while (top[a] != top[b]) {
		if (dep[top[a]] < dep[top[b]])swap(a, b);
		a = fa[top[a]];
	}
	if (dep[a] > dep[b])swap(a, b);
	return a;
}
int main() {

	scanf("%d%d%d", &n, &m, &root);
	for (int i = 1; i < n; i++) {
		int a, b; scanf("%d%d", &a, &b);
		addEdge(a, b); addEdge(b, a);
	}
	dfs1(root, 0);
	dfs2(root, root);
	while (m--) {
		int a, b; scanf("%d%d", &a, &b);
		printf("%d\n", lca(a, b));
	}
}

6.15 樹上啟發式合併

樹上啟發式合併 ( DSU on Tree) 是一種優雅的暴力

時間複雜度是 \(O(nlogn)\)

啟發式就是基於直覺或經驗的意思

樹上啟發式合併的程式碼很簡單

void dfs(int u,int p,bool keep){
	for(u.lightson v in u.son){
		dfs(v,u,false);
	}
	if(have u.heavyson)dfs(u.heavyson,u,true);;
	count();   //統計 u 及所有輕兒子的貢獻,並計算 u 的答案
	if(keep == false) del(); //如果不保留的話,刪除記錄
}

這段程式碼多看幾遍,自己多想一想

對於當前的節點 u,在執行 count() 計算節點 u 的答案的時候,其所有子節點的答案都已經被計算出來,並且只有 u 的重兒子及其子樹的記錄沒有被刪除,所以計算 u 的答案的時候只要暴力遍歷所有的輕兒子及其子樹就可以。

極其暴力(bushi

CF600E

首先要明白怎麼統計答案

int mp[N],mx,sum; //mp是顏色出現次數,mx是最多出現的次數,sum是當前的最優節點的和

mp[color]++;
if (mp[color] > mx){
	mx = mp[color];
	sum = color;
}
else if (mp[color] == mx) sum += color;

然後就是開始暴力合併(啟發式)

ll ans[N], sum, mx, mp[N];
void count(int u, int p,int hson) { //暴力算輕兒子及子樹的答案,此時重兒子及其子樹的資訊還在。
	mp[color[u]]++;
	if (mp[color[u]] > mx) {
		mx = mp[color[u]];
		sum = color[u];
	}
	else if (mp[color[u]] == mx) sum += color[u];

	repE(i, u) {
		int v = E[i].to;
		if (v == p or v == hson)continue;
		count(v, u, hson);
	}
}
void del(int u, int p) { //刪除子樹的資訊。
	mp[color[u]]--;
	repE(i, u) {
		int v = E[i].to;
		if (v == p)continue;
		del(v, u);
	}
}
void dfs(int u, int p, bool keep) {
	
	repE(i, u) {
		int v = E[i].to;
		if (v == p or v == son[u])continue;
		dfs(v, u, false); //
	}
	if (son[u])dfs(son[u], u, true);
	count(u, p, son[u]); ans[u] = sum;
	if (not keep) {
		sum = mx = 0;
		del(u,p);
	}
}

完整程式碼

#include<bits/stdc++.h>
#define repE(i,u) for(int i = head[u]; ~i;i = E[i].next)
using namespace std;

const int N = 1e5 + 10;
typedef long long ll;
int fa[N], sz[N], dep[N], son[N], color[N];
struct Edge { int to, next; }E[N << 1];
int head[N], tot;
void addEdge(int from, int to) { E[tot] = { to,head[from] }; head[from] = tot++; }


void dfs1(int u, int p) {
	fa[u] = p;
	dep[u] = dep[p] + 1;
	sz[u] = 1;
	int mx = -1;
	repE(i, u) {
		int v = E[i].to;
		if (v == p)continue;
		dfs1(v, u);
		sz[u] += sz[v];
		if (sz[v] > mx)mx = sz[v], son[u] = v;
	}
}

ll ans[N], sum, mx, mp[N];
void count(int u, int p,int hson) {
	mp[color[u]]++;
	if (mp[color[u]] > mx) {
		mx = mp[color[u]];
		sum = color[u];
	}
	else if (mp[color[u]] == mx) sum += color[u];

	repE(i, u) {
		int v = E[i].to;
		if (v == p or v == hson)continue;
		count(v, u, hson);
	}
}
void del(int u, int p) {
	mp[color[u]]--;
	repE(i, u) {
		int v = E[i].to;
		if (v == p)continue;
		del(v, u);
	}
}
void dfs(int u, int p, bool keep) {

	repE(i, u) {
		int v = E[i].to;
		if (v == p or v == son[u])continue;
		dfs(v, u, false);
	}
	if (son[u])dfs(son[u], u, true);
	count(u, p, son[u]); ans[u] = sum;
	if (not keep) {
		sum = mx = 0;
		del(u,p);
	}
}

int n;
int main() {
    ios::sync_with_stdio(0);
	cin >> n; memset(head, -1, sizeof head);
	for (int i = 1; i <= n; i++)cin >> color[i];
	for (int i = 1; i < n; i++) {
		int a, b; cin >> a >> b;
		addEdge(a, b); addEdge(b, a);
	}
	dfs1(1, 0);
	dfs(1, 0, true);
	for (int i = 1; i <= n; i++)cout << ans[i] << " \n"[i == n];
}

七、圖論

7.1 最短路

Dijkstra

/*
 * @Author: zhl
 * @LastEditTime: 2020-12-10 11:07:35
 */
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10, M = 1e6 + 10;

struct Edge {
	int to, next, w;
}E[M];

int head[N], tot;
void addEdge(int from, int to, int w) {
	E[tot] = Edge{ to,head[from],w };
	head[from] = tot++;
}
void init() {
	memset(head, -1, sizeof head);
}
int n, m, s;
int dis[N], vis[N];
typedef pair<int, int> p;
void Dijkstra() {

	for (int i = 0; i <= n; i++)dis[i] = 0x7fffffff;
	memset(vis, 0, sizeof vis);
	priority_queue<p,vector<p>,greater<p>>Q;
	Q.push({ 0,s }); dis[s] = 0;
	while (not Q.empty()) {
		int d = Q.top().first, u = Q.top().second; Q.pop();
		if (vis[u])continue;
		vis[u] = 1;
		for (int i = head[u]; ~i; i = E[i].next) {
			int v = E[i].to;
			if (not vis[v] and dis[v] > d + E[i].w) {
				dis[v] = dis[u] + E[i].w;
				Q.push({ dis[v],v });
			}
		}
	}
}
int main() {
	init();
	scanf("%d%d%d", &n, &m, &s);
	for (int i = 1; i <= m; i++) {
		int a, b, c; scanf("%d%d%d", &a, &b, &c);
		addEdge(a, b, c);
	}
	Dijkstra();
	for (int i = 1; i <= n; i++)printf("%d%c", dis[i], " \n"[i == n]);
}

vector版本

#include<bits/stdc++.h>
using namespace std;
const int N = 1e4 + 10;
typedef pair<int, int> p;
vector<p>G[N];
int dis[N], vis[N], n, m, s;
void Dijkstra() {
	for (int i = 0; i <= n; i++)dis[i] = 0x7fffffff;
	dis[s] = 0;
	priority_queue<p, vector<p>, greater<p>>Q;
	Q.push({ 0,s });
	while (not Q.empty()) {
		int d = Q.top().first, u = Q.top().second; Q.pop();
		if (vis[u])continue; vis[u] = 1;
		for (p e : G[u]) {
			if (not vis[e.first] and dis[e.first] > d + e.second) {
				dis[e.first] = d + e.second;
				Q.push({ dis[e.first],e.first });
			}
		}
	}
}
int main() {
	scanf("%d%d%d", &n, &m, &s);
	for (int i = 0; i < m; i++) {
		int a, b, c; scanf("%d%d%d", &a, &b, &c);
		G[a].push_back({ b,c });
	}
	Dijkstra();
	for (int i = 1; i <= n; i++)printf("%d%c", dis[i], " \n"[i == n]);
}

SPFA

#include<bits/stdc++.h>
#define to first
#define di second 
using namespace std;
const int N = 1e5 + 10;
typedef pair<int, int> p;
vector<p>G[N];

int dis[N], inq[N], n, m, s;
deque<int>Q;
void slf() {
	if (Q.size() > 2 and dis[Q.front()] > dis[Q.back()])swap(Q.front(), Q.back());
}
void spfa() {
	memset(dis, 63, sizeof dis);
	Q.push_back(s); inq[s] = 1; dis[s] = 0;
	while (not Q.empty()) {
		int u = Q.front(); Q.pop_front();
		slf();
		inq[u] = 0;
		for (p e : G[u]) {
			int v = e.to, d = e.di;
			if (dis[v] > dis[u] + d) {
				dis[v] = dis[u] + d;
				if (not inq[v]) {
					inq[v] = 1;
					Q.push_back(v);
					slf();
				}
			}
		}
	}
}
int main() {
	scanf("%d%d%d", &n, &m, &s);
	for (int i = 0; i < m; i++) {
		int a, b, c; scanf("%d%d%d", &a, &b, &c);
		G[a].push_back({ b,c });
	}
	spfa();
	for (int i = 1; i <= n; i++) {
		//cout << dis[i] << " \n"[i == n];
		printf("%d%c", dis[i], " \n"[i == n]);
	}
}

7.2 k短路

來自一年前的模板

#include<iostream>
#include<queue>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

const int maxn = 1e5 + 100;

struct E {
	int to, w, next;
}Edgez[maxn], Edgef[maxn];

int headz[maxn], headf[maxn];
int cntz, cntf;
typedef pair<int, int> P;
int dis[maxn];
int vis[maxn];
int N, M;

struct Node {
	int v, w;
	bool operator < (const Node& a)const {
		return w + dis[v] > a.w + dis[a.v];
	}
};

void init() {
	memset(headz, -1, sizeof(headz));
	memset(headf, -1, sizeof(headf));

	cntz = cntf = 0;
}

void addz(int from, int to, int w) {
	Edgez[cntz] = E{ to,w,headz[from] };
	headz[from] = cntz++;
}

void addf(int from, int to, int w) {
	Edgef[cntf] = E{ to,w,headf[from] };
	headf[from] = cntf++;
}

void dij(int s) {
	memset(vis, 0, sizeof(vis));
	memset(dis, 0x3f, sizeof(dis));
	dis[s] = 0;
	priority_queue<P, vector<P>, greater<P> >Q;

	Q.push(pair<int,int>{ 0,s });

	while (!Q.empty()) {
		int u = Q.top().second;
		Q.pop();

		if (vis[u])continue;

		vis[u] = 1;

		for (int i = headf[u]; i != -1; i = Edgef[i].next) {
			int v = Edgef[i].to;
			if (dis[u] + Edgef[i].w < dis[v]) {
				dis[v] = dis[u] + Edgef[i].w;;
				Q.push(pair<int,int>{ dis[v],v });
			}
		}
	}
}

int AStar(int s, int t, int k) {
	if (dis[s] == 0x3f3f3f3f) {
		return -1;
	}

	int cnt = 0;
	priority_queue<Node>Q;
	Q.push(Node{ s,0 });

	while (!Q.empty()) {
		Node tp = Q.top(); Q.pop();

		if (tp.v == t) {
			cnt++;
			if (cnt == k) {
				return tp.w;
			}
		}
		for (int i = headz[tp.v]; i != -1; i = Edgez[i].next) {
			Q.push(Node{ Edgez[i].to,Edgez[i].w + tp.w });
		}
	}
	return -1;
}

int main() {
	scanf("%d%d", &N, &M);
	init();
	for (int i = 1; i <= M; i++) {
		int from, to, w;
		scanf("%d%d%d", &from, &to, &w);
		addz(from, to, w);
		addf(to, from, w);
	}

	int s, t, k;
	scanf("%d%d%d", &s, &t, &k);

	dij(t);

	if (s == t) {
		k++;
	}

	cout << AStar(s, t, k) << endl;
}

7.3 網路流

1. 基本概念

流網路

\(G = (V,E)\)

有向圖,一個源點,一個匯點。每條邊都有一個屬性(容量)。

不考慮反向邊

可行流

可行流 \(f\) 需要滿足

  • 容量限制 ( \(0 \le f(u,v) \le c(u,v)\) )
  • 流量守恆 (除了源點和匯點,其餘點的流入等於流出)

\(|f|\) 表示可行流的流量

\[|f| = \sum_{(s,v)\in E} f(s,v) - \sum_{(s,v)\in E} f(v,s) \]

最大流

指的是 最大可行流

殘留網路

針對流網路的某一條流

記作 \(G_f\) ,是由流 \(f\) 決定的

\(V_f = V\) , \(E_f = E + E\) 的反向邊

\[c'(u,v) = \begin{cases}c(u,v) - f(u,v) & (u,v)\in E\\f(u,v)\end{cases} \]

殘留網路 + 殘留網路的一個可行流 = 原流網路的一個可行流

增廣路徑

在殘留網路中從源點出發邊權都大於0,到匯點結束的簡單路徑叫做增廣路徑。

  • 增廣路徑一定是一個可行流
  • 增廣路徑流量大於0

若對於一個可行流 \(f\) ,其 殘留網路 \(G_f\) 若不存在增廣路,則可以斷定 \(f\) 是一個最大流

流網路 \(G = (V,E)\)

把點集 \(V\) 分成 \(S\), \(T\) , \(S \cap T = \empty\) , \(S \cup T = V\)

\(s \in S\) , \(t \in T\)

割的容量

所有從 \(S\)\(T\) 的邊的容量之和 \(c(S,T) = \sum_{u\in S} \sum_{v \in T} c(u,v)\)

最小割指割的容量的最小值

割的流量

\(f(S,T) = \sum_{u\in S}\sum_{v\in T}f(u,v) - \sum_{v\in T}\sum_{u\in S}f(v,u)\)

對任意的割,任意的一個可行流,割的流量一定小於等於割的容量

$\forall S,T \ \ \forall f , f(S,T) \le c(S,T) $

對任意的割,任意的可行流,一定有割的流量等於可行流的流量
\(\forall S,T\ \ \forall f \\|f| = f(S,T)\)

證明

首先有

\(f(S,T) = -f(T,S)\) , \(f(X,X)=0\)

\(f(S,X \cup Y) = f(S,X) + f(S,Y)\) , \(X \cap Y = \empty\)

\(f(X \cup Y, T) = f(X,T) + f(Y,T) , X \cap Y = \empty\)

\[f(S,T) = f(S,V) - f(S,S)\\=f(S,V)\\=f(s,V) + f(S-s,V)\\=f(s,V) =|f| \]

最大流最小割定理

  • \(f\) 是最大流
  • \(f\) 的殘留網路中不存在增廣路
  • 存在某個割 \([S,T]\) , \(|f| = c(S,T)\)

三者相互等價

一推二和三推一都比較簡單

  • ①推②

反證,若存在增廣路,則可以更大流量

  • ③推①

最大流 \(\ge |f|\)

最大流 \(\le c(S,T) = |f|\)

所以 \(|f|\) = 最大流

最小割 \(\le c(S,T) = |f| \le\) 最大流

此時還有 最小割=最大流

  • ②推③

\(S\) , 在 \(G_f\) 中,從 \(s\) 出發沿容量大於0的邊走,所有能走到的點

\(T = V - S\) ,因為沒有增廣路,所以 \(S,T\) 是一個割

\(x\in S,y \in T\) , 有 \(f(x,y) = c(x,y)\) ,反向邊 \(f(y,x) = 0\)

所以 \(|f| = c(S,T)\)

/*
 * @Author: zhl
 * @Date: 2020-10-20 11:09:59
 */
#include<bits/stdc++.h>
using namespace std;

const int N = 2e6 + 10, M = 2e6 + 10, inf = 1e9;
int head[N],cur[N],dis[N],tot;
int n,m,s,t;

struct Edge{int to, next, flow;}E[M<<1];
void addEdge(int from,int to,int w){
    E[tot] = {to,head[from],w};head[from] = tot++;
    E[tot] = {from,head[to],0};head[to] = tot++;
}

bool bfs(){
    queue<int>Q;memset(dis,-1,sizeof dis);
    dis[s] = 0;cur[s] = head[s];Q.push(s);
    while(!Q.empty()){
        int u = Q.front();Q.pop();
        for(int i = head[u];~i;i = E[i].next){
            int v = E[i].to;
            if(dis[v] == -1 and E[i].flow){
                Q.push(v);dis[v] = dis[u] + 1;cur[v] = head[v];
                if(v == t)return true;
            }
        }
    }
    return false;
}
int dfs(int u,int limit){
    if(u == t)return limit;
    int k,res = 0;
    for(int i = cur[u]; ~i and res < limit;i = E[i].next){
        int v = E[i].to;cur[u] = i;
        if(dis[v] == dis[u] + 1 and E[i].flow){
            k = dfs(v,min(E[i].flow,limit-res));
            if(!k)dis[v] = -1;
            E[i].flow -= k;E[i^1].flow += k;res += k;
        }
    }
    return res;
}
int Dinic(){
    int f,res = 0;
    while(bfs())while(f=dfs(s,inf))res += f;
    return res;
}

2. 基本演算法

EK : \(O(nm^2)\)

Dinic: \(O(n^2m)\)

但其實上界非常寬鬆,一般 EK 能處理 \(10^3-10^4\) 的資料,Dinic \(10^4-10^5\)

EK演算法

一般求最大流用不到,求最小費用流的時候 EK是核心演算法

dinic

最大流

3.上下界流

無源匯上下界可行流

給有向圖 G, 每條邊都有一個流量上界和流量下界。若存在可行流,則輸出每條邊的流量,若不存在輸出“NO“

思路:

對於每條邊,先流下界,統計每個點的流量,所有點的流量和一定是0,建立一個超級源點連線所有流量為正的點,超級匯點連線流量為負的點,跑最大流,看能不能跑滿流。因為所有流量為正的點表示有多,需要流出去,所以要連源點。

/*
 * @Author: zhl
 * @Date: 2020-10-20 11:09:59
 */

int val[N];
int minF[N];
signed main() {
	scanf("%d%d", &n, &m);
	memset(head, -1, sizeof(int) * (n + 10));
	for (int i = 1; i <= m; i++) {
		int a, b, c, d;
		scanf("%d%d%d%d", &a, &b, &c, &d);
		addEdge(a, b, d - c);
		val[a] -= c;
		val[b] += c;
		minF[i - 1] = c;
	}
	int sum = 0;
	for (int i = 1; i <= n; i++) {
		if (val[i] > 0)addEdge(0, i, val[i]), sum += val[i];
		if (val[i] < 0)addEdge(i, n + 1, -val[i]);
	}
	s = 0, t = n + 1;
	int maxflow = Dinic();

	if (maxflow == sum) {
		printf("YES\n");
		for (int i = 0; i < m; i++) {
			printf("%d\n", E[(2*i)^1].flow + minF[i]);
		}
	}
	else {
		printf("NO\n");
	}
}

有源匯上下界最大流

還是像無源匯上下界那樣。

addEdge(t,s,inf) ,這麼一加,然後跑一遍超級源點到超級匯點的可行流

加的這條邊的反向邊,就是一個 s 到 t 的基礎流

然後再刪掉這邊,在此時的殘留網路中跑一遍 s 到 t 的最大流

兩個答案加起來就是 有源匯上下界最大流

/*
 * @Author: zhl
 * @Date: 2020-10-20 11:09:59
 */
#include<bits/stdc++.h>
 //#define int long long
using namespace std;

const int N = 1e4 + 10, M = 1e5 + 10, inf = 1e9;
int n, m, s, t, tot, head[N];
int ans, dis[N], cur[N];

struct Edge {
	int to, next, flow;
}E[M << 1];

void addEdge(int from, int to, int w) {
	E[tot] = Edge{ to,head[from],w };
	head[from] = tot++;
	E[tot] = Edge{ from,head[to],0 };
	head[to] = tot++;
}

int bfs() {
	for (int i = 0; i <= n + 1; i++) dis[i] = -1;
	queue<int>Q;
	Q.push(s);
	dis[s] = 0;
	cur[s] = head[s];

	while (!Q.empty()) {
		int u = Q.front();
		Q.pop();
		for (int i = head[u]; ~i; i = E[i].next) {
			int v = E[i].to;
			if (E[i].flow && dis[v] == -1) {
				Q.push(v);
				dis[v] = dis[u] + 1;
				cur[v] = head[v];
				if (v == t)return 1; //分層成功
			}
		}
	}
	return 0;
}

int dfs(int x, int sum) {
	if (x == t)return sum;
	int k, res = 0;
	for (int i = cur[x]; ~i && res < sum; i = E[i].next) {
		cur[x] = i;
		int v = E[i].to;
		if (E[i].flow > 0 && (dis[v] == dis[x] + 1)) {
			k = dfs(v, min(sum, E[i].flow));
			if (k == 0) dis[v] = -1; //不可用
			E[i].flow -= k; E[i ^ 1].flow += k;
			res += k; sum -= k;
		}
	}
	return res;
}

int Dinic() {
	int ans = 0;
	while (bfs()) {
		ans += dfs(s, inf);
	}
	return ans;
}

int val[N];
int minF[N];
signed main() {
	int S, T;
	scanf("%d%d%d%d", &n, &m, &S, &T);
	memset(head, -1, sizeof(int) * (n + 10));
	for (int i = 1; i <= m; i++) {
		int a, b, c, d;
		scanf("%d%d%d%d", &a, &b, &c, &d);
		addEdge(a, b, d - c);
		val[a] -= c;
		val[b] += c;
		minF[i - 1] = c;
	}
	int sum = 0;
	for (int i = 1; i <= n; i++) {
		if (val[i] > 0)addEdge(0, i, val[i]), sum += val[i];
		if (val[i] < 0)addEdge(i, n + 1, -val[i]);
	}
	s = 0, t = n + 1;
	addEdge(T, S, inf);
	int maxflow = Dinic();

	if (maxflow == sum) {
		int base = E[tot - 1].flow;
		E[tot - 1].flow = E[tot - 2].flow = 0;
		s = S; t = T;
		printf("%d\n", base + Dinic());
	}
	else {
		printf("No Solution\n");
	}
}

有源匯上下界最小流

跟上面類似,跑 ts 的最大流,減去就可以了

/*
 * @Author: zhl
 * @Date: 2020-10-20 11:09:59
 */
#include<bits/stdc++.h>
 //#define int long long
using namespace std;

const int N = 2e6 + 10, M = 2e6 + 10, inf = 1e9;
int n, m, s, t, tot, head[N];
int ans, dis[N], cur[N];

struct Edge {
	int to, next, flow;
}E[M << 1];

void addEdge(int from, int to, int w) {
	E[tot] = Edge{ to,head[from],w };
	head[from] = tot++;
	E[tot] = Edge{ from,head[to],0 };
	head[to] = tot++;
}

int bfs() {
	for (int i = 0; i <= n + 1; i++) dis[i] = -1;
	queue<int>Q;
	Q.push(s);
	dis[s] = 0;
	cur[s] = head[s];

	while (!Q.empty()) {
		int u = Q.front();
		Q.pop();
		for (int i = head[u]; ~i; i = E[i].next) {
			int v = E[i].to;
			if (E[i].flow && dis[v] == -1) {
				Q.push(v);
				dis[v] = dis[u] + 1;
				cur[v] = head[v];
				if (v == t)return 1; //分層成功
			}
		}
	}
	return 0;
}

int dfs(int x, int sum) {
	if (x == t)return sum;
	int k, res = 0;
	for (int i = cur[x]; ~i && res < sum; i = E[i].next) {
		cur[x] = i;
		int v = E[i].to;
		if (E[i].flow > 0 && (dis[v] == dis[x] + 1)) {
			k = dfs(v, min(sum, E[i].flow));
			if (k == 0) dis[v] = -1; //不可用
			E[i].flow -= k; E[i ^ 1].flow += k;
			res += k; sum -= k;
		}
	}
	return res;
}

int Dinic() {
	int ans = 0;
	while (bfs()) {
		ans += dfs(s, inf);
	}
	return ans;
}

int val[N];
int minF[N];
signed main() {
	int S, T;
	scanf("%d%d%d%d", &n, &m, &S, &T);
	memset(head, -1, sizeof(int) * (n + 10));
	for (int i = 1; i <= m; i++) {
		int a, b, c, d;
		scanf("%d%d%d%d", &a, &b, &c, &d);
		addEdge(a, b, d - c);
		val[a] -= c;
		val[b] += c;
		minF[i - 1] = c;
	}
	int sum = 0;
	for (int i = 1; i <= n; i++) {
		if (val[i] > 0)addEdge(0, i, val[i]), sum += val[i];
		if (val[i] < 0)addEdge(i, n + 1, -val[i]);
	}
	s = 0, t = n + 1;
	addEdge(T, S, inf);
	int maxflow = Dinic();

	if (maxflow == sum) {
		int base = E[tot - 1].flow;
		E[tot - 1].flow = E[tot - 2].flow = 0;
		s = T; t = S;//只有這裡改動了
		printf("%d\n", base - Dinic());
	}
	else {
		printf("No Solution\n");
	}
}

4.最小割

最大權閉合子圖

閉合子圖

一個點集,不存在連線點集和點集外的邊。

自閉集合,跟外界沒有接觸,邊是自洽的。

最大權閉合子圖

點權和最大的閉合子圖

把閉合子圖的集合對映到流網路的割的集合

原圖 \(G(V,E)\)

建一個源點 \(s\) 到所有正權點,容量是點權,建一個匯點 \(t\) 到所有負權點,容量是點權絕對值。原圖的所有邊不變,容量 \(+\infty\)

原圖 \(G\)

新圖 \(G'\)

簡單割

所有的割邊都連線 \(s\)\(t\)

簡單割是一個很重要的概念。

現證明 簡單割與閉合子圖一一對應

對與閉合子圖 \(V\) , $ V$ 中的點只能連線 \(s\)\(t\) ,所以是簡單割

對於一個簡單割\([S,T]\)\(S-\{s\}\) 是一個閉合圖,因為連線 \(S\)\(T\) 的邊必定與 \(s\)\(t\) 相連,所有 \(S-{s}\) 中的邊只能連線自己

現在考慮數值上的關係

對於閉合子圖 \(V\) , \(V^+\) 表示 \(V\) 中所有正權點的點權之和, \(V^-\) 表示所有負權的的點權絕對值之和

\(V\) 的權和為

\[s = V^+ - V^- \]

現在考慮割 \([S,T]\) 的流量, \(S \to T\) 的只有兩種邊,一種是 \(V\)\(t\) 的邊,一種是 \(s\) 到 原圖中除去 \(V\) 的其他點, 由於 \(s\) 連線的是正權的點,設原圖所有正權點的點權之和是 \(sum\) ,則這部分正權點的點權之和是 \(sum - V^+\)

\[c = V^- + sum - V^+ = sum - s \]

所以至此,要求最大權閉合子圖,也就是求圖 \(G'\) 的最小割

[NOI2006]最大獲利

建基站需要花費 \(c_i\) , 使用者需要 基站 \(a,b\) , 能獲利 \(w_i\)

對於每個使用者建立兩條有向邊代表若選了這個使用者則這兩個基站也要選,跑最大權閉合子圖

/*
 * @Author: zhl
 * @Date: 2020-10-26 16:03:03
 */
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i = a;i <= b;i++)
#define repE(i,u) for(int i = head[u];~i;i = E[i].next)

using namespace std;

const int N = 6e4 + 10, M = 2e5 + 10, inf = 1e8;
int head[N], cur[N], tot, dis[N];
int n, m, s, t;
struct Edge {
	int to, next, flow;
}E[M << 1];

void addEdge(int from, int to, int w) {
	//cout << from << " -> " << to << " : " << w << endl;
	E[tot] = Edge{ to,head[from],w };
	head[from] = tot++;
	E[tot] = Edge{ from,head[to],0 };
	head[to] = tot++;
}

bool bfs() {
	memset(dis, -1, sizeof dis);
	queue<int>Q; Q.push(s); cur[s] = head[s]; dis[s] = 0;

	while (!Q.empty()) {
		int u = Q.front(); Q.pop();
		repE(i, u) {
			int v = E[i].to;
			if (dis[v] == -1 and E[i].flow) {
				Q.push(v);
				dis[v] = dis[u] + 1;
				cur[v] = head[v];
				if (v == t)return true;
			}
		}
	}
	return false;
}

int dfs(int u, int limit) {
	if (u == t)return limit;
	int k, res = 0;
	for (int i = cur[u]; ~i and res < limit; i = E[i].next) {
		int v = E[i].to;
		cur[u] = i;
		if (dis[v] == dis[u] + 1 and E[i].flow) {
			k = dfs(v, min(E[i].flow, limit - res));
			if (!k)dis[v] = -1;
			E[i].flow -= k; E[i ^ 1].flow += k;
			res += k;
		}
	}
	return res;
}

int Dinic() {
	int f, res = 0;
	while (bfs())while (f = dfs(s, inf))res += f;
	return res;
}

int val[N];
int main() {
	scanf("%d%d", &n, &m);
	memset(head, -1, sizeof head);
	s = 0; t = n + m + 1;

	rep(i, 1, n) {
		scanf("%d", val + i);
		addEdge(i, t, val[i]);
	}
	int sum = 0;
	rep(i, 1, m) {
		int a, b, c; scanf("%d%d%d", &a, &b, &c); sum += c;
		val[n + i] = c; addEdge(n + i, a, inf); addEdge(n + i, b, inf);
		addEdge(s, n + i, c);
	}

	printf("%d\n", sum - Dinic());
}

最大密度子圖

參考

胡伯濤論文

部落格總結

無向圖 \(G(V,E)\) 的密度 \(D = \dfrac {|E|} {|V|}\)

若選擇了邊 \((u,v)\) 則必須有 \(u \in V , v \in V\)

最大密度子圖

具有最大密度的子圖,最大化 \(D' = \dfrac {|E'|} {|V'|}\)

根據之前的分數規劃

可以二分答案

並且有如下引理

  • 任意兩個子圖的密度差 \(\ge \dfrac 1{n^2}\)

\(\dfrac {m_1}{n_1} - \dfrac {m_2}{n_2} = \dfrac {m_1n_2 - m_2n_1} {n_1n_2} \ge \dfrac 1 {n_1n_2} \ge \dfrac 1 {n^2}\)

現在的主要問題是如何 \(Judge\)

設要檢驗的值為 \(g\) , 建構函式 $f = |E| - g|V| $ , 現在的目標是使得 \(f\) 最大

有兩種方法

1. 最大權閉合子圖

目標:

最大化 \(f = |E| - g|V|\)

把無向邊 \((u,v)\) 看做一個點連線兩條有向邊指向 \(u\)\(v\) 原圖的點權值設為 \(-g\)

邊的點為 \(1\) ,這樣就轉成了最大權閉合子圖的問題

2.優化演算法(誘導子圖最小割)

對於點集 \(V'\) , 顯然能選的邊要儘可能的都選上

所有能選的邊為 \(V'\) 中點的度的和減去割 \([V',\overline{V'}]\) 的容量的一半

\[|E'| = \dfrac {\sum_{v\in V'}deg(v) -c[V',\overline{V'}]}2 \]

\[f = \dfrac 12\big(\sum_{v\in V'}deg(v)-c[V',\overline{V'}] - 2\sum_{v \in V'}g\big) \\ = \dfrac 12\big(\sum_{v\in V'}(deg(v)-2g)-c[V',\overline{V'}] \big) \]

按如下建圖, \(U\) 是一個大常數,保證邊權不是負數

\([S,T]\) , \(S = {s} + V'\) ,\(T = {t} + \overline{V'}\)

割的容量 \(c[S,T]\) 有四個部分

\(s\to t\)\(0\)

\(s \to \overline{V'}\) : \(\sum_{v \in \overline{V'}}U\)

\(V' \to \overline{V'}\) : 其實就是 \(c[V',\overline{V'}]\)

\(V' \to t\)\(\sum_{v\in V'}U+2g-d_v\)

\[c[S,T] = c[V',\overline{V'}] + Un + \sum_{v \in V'}2g-d_v = Un -2f \]

所以最大化 \(f\) 即最小化 \(c[S,T]\) ,求最小割就可以了

5. 習題

東方文花帖|【模板】有源匯上下界最大流

說是模板,其實題意也不是那麼簡單易懂。

認真閱讀題目後,建出下面的圖

做完這個題可以更加理解 有源匯上下界最大流

源點全是出邊,匯點全是入邊會更好理解。

此時加的addEdge(t,s,inf) 的反向邊的流量就是基礎流量,然後去掉再跑最大流,兩個流加起來。

左邊的是 1-n 天, 右邊是 1-m 個美少女

問題就是求 st 的有源匯上下界最大流

注意美少女的編號從 0 開始

/*
 * @Author: zhl
 * @Date: 2020-10-20 11:09:59
 */
#include<bits/stdc++.h>
 //#define int long long
using namespace std;

const int N = 2e6 + 10, M = 2e6 + 10, inf = 1e9;
int n, m, s, t, tot, head[N];
int ans, dis[N], cur[N];

struct Edge {
	int to, next, flow;
}E[M << 1];

void addEdge(int from, int to, int w) {
	E[tot] = Edge{ to,head[from],w };
	head[from] = tot++;
	E[tot] = Edge{ from,head[to],0 };
	head[to] = tot++;
}

int bfs() {
	for (int i = 0; i <= n + m + 3; i++) dis[i] = -1;
	queue<int>Q;
	Q.push(s);
	dis[s] = 0;
	cur[s] = head[s];

	while (!Q.empty()) {
		int u = Q.front();
		Q.pop();
		for (int i = head[u]; ~i; i = E[i].next) {
			int v = E[i].to;
			if (E[i].flow && dis[v] == -1) {
				Q.push(v);
				dis[v] = dis[u] + 1;
				cur[v] = head[v];
				if (v == t)return 1; //分層成功
			}
		}
	}
	return 0;
}

int dfs(int x, int sum) {
	if (x == t)return sum;
	int k, res = 0;
	for (int i = cur[x]; ~i && res < sum; i = E[i].next) {
		cur[x] = i;
		int v = E[i].to;
		if (E[i].flow > 0 && (dis[v] == dis[x] + 1)) {
			k = dfs(v, min(sum, E[i].flow));
			if (k == 0) dis[v] = -1; //不可用
			E[i].flow -= k; E[i ^ 1].flow += k;
			res += k; sum -= k;
		}
	}
	return res;
}

int Dinic() {
	int ans = 0;
	while (bfs()) {
		ans += dfs(s, inf);
	}
	return ans;
}

int val[N];
int minF[N];
signed main() {
	while(~scanf("%d%d",&n,&m)){
		memset(head,-1,sizeof(int)*(n+m+10));
		memset(val,0,sizeof(int)*(n+m+10));
		tot = 0;
		for(int i = 1;i <= m;i++){
			int x;scanf("%d",&x);
			addEdge(n+i,n+m+1,inf-x);
			val[n+i]-=x;
			val[n+m+1]+=x;
		}
		
		for(int i = 1;i <= n;i++){
			int c,d;
			scanf("%d%d",&c,&d);
			addEdge(0,i,d);
			val[0] -= 0;
			val[i] += 0;
			for(int j = 1;j <= c;j++){
				int u,l,r;
				scanf("%d%d%d",&u,&l,&r);
				addEdge(i,u+n+1,r-l);
				val[i] -= l;
				val[u+n+1] += l;
			}
		}
		
		s = n + m + 2;t = n + m + 3;
		int sum = 0;
		for(int i = 0;i <= n + m + 1;i++){
			if(val[i] > 0){
				addEdge(s,i,val[i]);
				sum += val[i];
			}
			if(val[i] < 0)addEdge(i,t,-val[i]);
		}
		addEdge(n+m+1,0,inf);
        int dd = Dinic();
		//cout << dd << endl;
        //cout << sum << endl;
		if(dd == sum){
			int res = E[tot-1].flow;
			E[tot-1].flow = E[tot-2].flow = 0;
            s = 0;t = n + m + 1;
			printf("%d\n",res + Dinic());
		}
		else printf("-1\n");
        puts("");
    }
}

多源匯最大流

很簡單,建一個超級源點連線所有的源點,建一個超級匯點連線所有的匯點。

/*
 * @Author: zhl
 * @Date: 2020-10-20 11:09:59
 */
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i = a;i <= b;i++)
#define repE(i,u) for(int i = head[u];i;i = E[i].next)
//#define int long long
using namespace std;

const int N = 2e6 + 10, M = 2e6 + 10, inf = 1e9;
int n, m, s, t, tot, head[N];
int ans, dis[N], cur[N];

struct Edge {
	int to, next, flow;
}E[M<<1];

void addEdge(int from, int to, int w) {
	E[tot] = Edge{ to,head[from],w };
	head[from] = tot++;
	E[tot] = Edge{ from,head[to],0 };
	head[to] = tot++;
}

int bfs() {
	for (int i = 0; i <= n + 1; i++) dis[i] = -1;
	queue<int>Q;
	Q.push(s);
	dis[s] = 0;
	cur[s] = head[s];

	while (!Q.empty()) {
		int u = Q.front();
		Q.pop();
		for (int i = head[u]; ~i; i = E[i].next) {
			int v = E[i].to;
			if (E[i].flow && dis[v] == -1) {
				Q.push(v);
				dis[v] = dis[u] + 1;
				cur[v] = head[v];
				if (v == t)return 1; //分層成功
			}
		}
	}
	return 0;
}

int dfs(int x, int sum) {
	if (x == t)return sum;
	int k, res = 0;
	for (int i = cur[x]; ~i && res < sum; i = E[i].next) {
		cur[x] = i;
		int v = E[i].to;
		if (E[i].flow > 0 && (dis[v] == dis[x] + 1)) {
			k = dfs(v, min(sum, E[i].flow));
			if (k == 0) dis[v] = -1; //不可用
			E[i].flow -= k;E[i ^ 1].flow += k;
			res += k;sum -= k;
		}
	}
	return res;
}

int Dinic() {
	int ans = 0;
	while (bfs()) {
        ans += dfs(s,inf);
	}
	return ans;
}

signed main() {
	scanf("%d%d%d%d",&n,&m,&s,&t);
	memset(head, -1, sizeof(int) * (n + 10));
	rep(i,1,s){
        int x;scanf("%d",&x);
        addEdge(0,x,inf);
    }
    rep(i,1,t){
        int x;scanf("%d",&x);
        addEdge(x,n+1,inf);
    }
    rep(i,1,m){
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        addEdge(a,b,c);
    }
    s = 0,t = n + 1;
    printf("%d\n",Dinic());
}

關鍵邊

如果增加某一條邊的容量可以增加最大流,則為關鍵邊

跑一個最大流後,從源點bfs,從匯點bfs。交接處的邊就是關鍵邊

for (int i = 0; i < tot; i+=2) {
	if (!E[i].flow and vis_t[E[i].to] and vis_s[E[i ^ 1].to]) 			ans++;
}

這裡要 i+=2

/*
 * @Author: zhl
 * @Date: 2020-10-21 09:45:30
 */


#include<bits/stdc++.h>
#define rep(i,a,b) for(int i = a;i <= b;i++)
#define repE(i,u) for(int i = head[u];~i;i = E[i].next)
using namespace std;

const int N = 5e2 + 10, M = 5e3 + 10, inf = 1e9;
struct Edge {
	int to, flow, next;
}E[M << 1];
int head[N], tot;
void addEdge(int from, int to, int w) {
	E[tot] = Edge{ to,w,head[from] };
	head[from] = tot++;
	E[tot] = Edge{ from,0,head[to] };
	head[to] = tot++;
}

int n, m, s, t;
int dis[N], cur[N];
bool bfs() {
	rep(i, 0, n)dis[i] = -1;
	queue<int>Q;
	Q.push(s); dis[s] = 0; cur[s] = head[s];
	while (!Q.empty()) {
		int u = Q.front(); Q.pop();
		repE(i, u) {
			int v = E[i].to;
			if (dis[v] == -1 and E[i].flow) {
				cur[v] = head[v];
				dis[v] = dis[u] + 1;
				Q.push(v);
				if (v == t)return true;
			}
		}
	}
	return false;
}

int dfs(int u, int limit) {
	if (u == t)return limit;
	int k, res = 0;
	for (int i = cur[u]; ~i and res < limit; i = E[i].next) {
		int v = E[i].to;
		cur[u] = i;
		if (dis[v] == dis[u] + 1 and E[i].flow) {
			k = dfs(v, min(limit, E[i].flow));
			if (k == 0)dis[v] = -1;
			E[i].flow -= k; E[i ^ 1].flow += k;
			limit -= k; res += k;
		}
	}
	return res;
}

int Dinic() {
	int res = 0;
	while (bfs())res += dfs(s, inf);
	return res;
}

int vis_s[N], vis_t[N];
void Dfs(int u, int* vis, int p) {
	vis[u] = 1;
	repE(i, u) {
		int v = E[i].to;
		if (!vis[v] and E[i ^ p].flow) {
			Dfs(v, vis, p);
		}
	}
}
int main() {
	scanf("%d%d", &n, &m);
	memset(head, -1, sizeof head);
	rep(i, 1, m) {
		int a, b, c;
		scanf("%d%d%d", &a, &b, &c);
		addEdge(a + 1, b + 1, c);
	}
	s = 1, t = n;
	Dinic();

	Dfs(s, vis_s, 0); Dfs(t, vis_t, 1);
	int ans = 0;
	for (int i = 0; i < tot; i+=2) {
		if (!E[i].flow and vis_t[E[i].to] and vis_s[E[i ^ 1].to]) 		  {
			ans++;
		}
	}
	printf("%d\n", ans);
}

最大流判定

Poj 2455

\(N\) 個點 , \(P\) 條雙向邊,每條邊有長度,每條邊只能走一次。

需要從 \(1\)\(N\) ,進行 \(T\) 次。問經過的最大邊權的最小值。

保證可以在不走重複道路的情況走 \(T\)

思路:

二分答案

判定用不超過 \(m\) 的邊能不能到 \(T\) 次,跑網路流即可

/*
 * @Author: zhl
 * @Date: 2020-10-20 11:09:59
 */
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i = a;i <= b;i++)
#define repE(i,u) for(int i = head[u];~i;i = E[i].next)
using namespace std;

const int N = 2e6 + 10, M = 2e6 + 10, inf = 1e9;
struct Edge {
	int to, flow, next;
}E[M << 1];
int head[N], tot;
void addEdge(int from, int to, int w) {
	E[tot] = Edge{ to,w,head[from] };
	head[from] = tot++;
	E[tot] = Edge{ from,0,head[to] };
	head[to] = tot++;
}

int n, m, s, t, k;
int dis[N], cur[N];
bool bfs() {
	rep(i, 0, n)dis[i] = -1;
	queue<int>Q;
	Q.push(s); dis[s] = 0; cur[s] = head[s];
	while (!Q.empty()) {
		int u = Q.front(); Q.pop();
		repE(i, u) {
			int v = E[i].to;
			if (dis[v] == -1 and E[i].flow) {
				cur[v] = head[v];
				dis[v] = dis[u] + 1;
				Q.push(v);
				if (v == t)return true;
			}
		}
	}
	return false;
}

int dfs(int u, int limit) {
	if (u == t)return limit;
	int k, res = 0;
	for (int i = cur[u]; ~i and res < limit; i = E[i].next) {
		int v = E[i].to;
		cur[u] = i;
		if (dis[v] == dis[u] + 1 and E[i].flow) {
			k = dfs(v, min(limit, E[i].flow));
			if (k == 0)dis[v] = -1;
			E[i].flow -= k; E[i ^ 1].flow += k;
			limit -= k; res += k;
		}
	}
	return res;
}

vector<pair<int, int> >G[300];

int Dinic() {
	int res = 0;
	while (bfs())res += dfs(s, inf);
	return res;
}
bool judge(int m) {
	memset(head, -1, sizeof(int) * (n + 10));
	tot = 0;
	for (int i = 1; i <= n; i++) {
		for (auto e : G[i]) {
			if (e.second <= m) {
				addEdge(i, e.first, 1);
			}
		}
	}
	int maxflow = Dinic();
	return maxflow >= k;
}

int main() {
	scanf("%d%d%d", &n, &m, &k);
	s = 1, t = n;
	rep(i, 1, m) {
		int a, b, c; scanf("%d%d%d", &a, &b, &c);
		G[a].push_back({ b,c });
		G[b].push_back({ a,c });
	}
	int l = 0, r = 1000100;

	int ans = inf;
	while (l <= r) {
		int mid = l + r >> 1;
		if (judge(mid)) {
			ans = min(ans, mid);
			r = mid - 1;
		}
		else {
			l = mid + 1;
		}
	}
	printf("%d\n", ans);
}

家園 / 星際轉移問題

網路流24題

由於人類對自然資源的消耗,人們意識到大約在 2300 年之後,地球就不能再居住了。於是在月球上建立了新的綠地,以便在需要時移民。令人意想不到的是,2177 年冬由於未知的原因,地球環境發生了連鎖崩潰,人類必須在最短的時間內遷往月球。

現有 \(n\) 個太空站位於地球與月球之間,且有 \(m\) 艘公共交通太空船在其間來回穿梭。每個太空站可容納無限多的人,而太空船的容量是有限的,第 \(i\) 艘太空船隻可容納 \(h_i\) 個人。每艘太空船將週期性地停靠一系列的太空站,例如 \((1,3,4)\) 表示該太空船將週期性地停靠太空站 \(134134134\dots134134134\)…。每一艘太空船從一個太空站駛往任一太空站耗時均為 \(1\) 。人們只能在太空船停靠太空站(或月球、地球)時上、下船。

初始時所有人全在地球上,太空船全在初始站。試設計一個演算法,找出讓所有人儘快地全部轉移到月球上的運輸方案。

思路

按時間分層建圖,停留的話就建一條垂直的 inf 邊(綠邊)

每一輛飛船按下圖橙藍建圖,容量是載人數

/*
 * @Author: zhl
 * @Date: 2020-10-20 11:09:59
 */
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i = a;i <= b;i++)
#define repE(i,u) for(int i = head[u];~i;i = E[i].next)
using namespace std;

const int N = 2e6 + 10, M = 2e6 + 10, inf = 1e9;
struct Edge {
	int to, flow, next;
}E[M << 1];
int head[N], tot;
void addEdge(int from, int to, int w) {
	E[tot] = Edge{ to,w,head[from] };
	head[from] = tot++;
	E[tot] = Edge{ from,0,head[to] };
	head[to] = tot++;
}

int n, m, s, t, ans;
int dis[N], cur[N];
bool bfs() {
	rep(i, 0, (n+2)*(ans+1))dis[i] = -1;
	queue<int>Q;
	Q.push(s); dis[s] = 0; cur[s] = head[s];
	while (!Q.empty()) {
		int u = Q.front(); Q.pop();
		repE(i, u) {
			int v = E[i].to;
			if (dis[v] == -1 and E[i].flow) {
				cur[v] = head[v];
				dis[v] = dis[u] + 1;
				Q.push(v);
				if (v == t)return true;
			}
		}
	}
	return false;
}

int dfs(int u, int limit) {
	if (u == t)return limit;
	int k, res = 0;
	for (int i = cur[u]; ~i and res < limit; i = E[i].next) {
		int v = E[i].to;
		cur[u] = i;
		if (dis[v] == dis[u] + 1 and E[i].flow) {
			k = dfs(v, min(limit, E[i].flow));
			if (k == 0)dis[v] = -1;
			E[i].flow -= k; E[i ^ 1].flow += k;
			limit -= k; res += k;
		}
	}
	return res;
}

int Dinic() {
	int res = 0;
	while (bfs())res += dfs(s, inf);
	return res;
}
int H[N], k, r;
vector<int>G[30];
int fa[N];
int find(int a) { return a == fa[a] ? a : fa[a] = find(fa[a]); }
void merge(int a, int b) { if (find(a) != find(b))fa[find(a)] = find(b); }

int main() {
	scanf("%d%d%d", &n, &m, &k);
	rep(i, 0, n + 1)fa[i] = i;
	rep(i, 1, m) {
		scanf("%d%d", H + i, &r);
		rep(j, 1, r) {
			int x; scanf("%d", &x); if (x == -1)x = n + 1;
			G[i].push_back(x);
		}
		rep(j, 1, r - 1) {
			merge(G[i][0], G[i][j]);
		}
	}
	if (find(0) != find(n + 1)){
	    printf("0\n");
	    return 0;
	}
	ans = 1;
	while (1) {
		memset(head, -1, sizeof(int)* ((n + 10)* (ans + 1)));
		tot = 0;
		rep(i, 0, ans - 1) {
			rep(j, 1, m) {
				int len = G[j].size();
				int a = G[j][i % len] + (n + 2) * i;
				int b = G[j][(i + 1) % len] + (n + 2) * (i + 1);
				addEdge(a, b, H[j]);
			}
			rep(j, 0, n + 1) {

				addEdge((n + 2) * i + j, (n + 2) * (i + 1) + j, inf);
			}
		}
		s = 0, t = (n + 2) * (ans + 1) - 1;
		if (Dinic() >= k) {
			printf("%d\n", ans);
			return 0;
		}
		ans++;
	}

}

拆點

餐飲

農夫約翰一共烹製了 \(F\) 種食物,並提供了 \(D\) 種飲料。

約翰共有 \(N\) 頭奶牛,其中第 \(i\) 頭奶牛有 \(F_i\) 種喜歡的食物以及 \(D_i\) 種喜歡的飲料。

約翰需要給每頭奶牛分配一種食物和一種飲料,並使得有吃有喝的奶牛數量儘可能大。

每種食物或飲料都只有一份,所以只能分配給一頭奶牛食用(即,一旦將第 \(2\) 種食物分配給了一頭奶牛,就不能再分配給其他奶牛了)。

開始想的錯誤思路是拆牛,超級源點連所有的牛

正解:

/*
 * @Author: zhl
 * @Date: 2020-10-20 11:09:59
 */

/*
 * 拆點策略: 把一頭牛拆成兩頭,從源點到實物到牛再到飲料再到匯點 
 */
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i = a;i <= b;i++)
#define repE(i,u) for(int i = head[u];~i;i = E[i].next)
using namespace std;

const int N = 5e2 + 10, M = 5e3 + 10, inf = 1e9;
struct Edge {
	int to, flow, next;
}E[M << 1];
int head[N], tot;
void addEdge(int from, int to, int w) {
	E[tot] = Edge{ to,w,head[from] };
	head[from] = tot++;
	E[tot] = Edge{ from,0,head[to] };
	head[to] = tot++;
}

int n, m, s, t, P_nums;//圖中總點數[0-P_nums]

int dis[N], cur[N];
bool bfs() {
	rep(i, 0, P_nums)dis[i] = -1;
	queue<int>Q;
	Q.push(s); dis[s] = 0; cur[s] = head[s];
	while (!Q.empty()) {
		int u = Q.front(); Q.pop();
		repE(i, u) {
			int v = E[i].to;
			if (dis[v] == -1 and E[i].flow) {
				cur[v] = head[v];
				dis[v] = dis[u] + 1;
				Q.push(v);
				if (v == t)return true;
			}
		}
	}
	return false;
}

int dfs(int u, int limit) {
	if (u == t)return limit;
	int k, res = 0;
	for (int i = cur[u]; ~i and res < limit; i = E[i].next) {
		int v = E[i].to;
		cur[u] = i;
		if (dis[v] == dis[u] + 1 and E[i].flow) {
			k = dfs(v, min(limit, E[i].flow));
			if (k == 0)dis[v] = -1;
			E[i].flow -= k; E[i ^ 1].flow += k;
			limit -= k; res += k;
		}
	}
	return res;
}

int Dinic() {
	int res = 0;
	while (bfs())res += dfs(s, inf);
	return res;
}

int f, d;
int main() {
	scanf("%d%d%d", &n, &f, &d);
	P_nums = f + d + 2 * n + 1;
	memset(head, -1, sizeof(int) * (P_nums + 10));
	rep(i, 1, n) {
		int u = f + d + 2 * (i - 1) + 1;
		addEdge(u, u + 1, 1);
		int a, b, x;
		scanf("%d%d", &a, &b);
		rep(j, 1, a) { scanf("%d", &x); addEdge(x, u, 1); }
		rep(j, 1, b) { scanf("%d", &x); addEdge(u + 1, f + x, 1); }
	}
	rep(i, 1, f)addEdge(0, i, 1);
	rep(i, f + 1, f + d)addEdge(i, P_nums, 1);
	s = 0, t = P_nums;

	printf("%d\n", Dinic());
}

【最長不下降子序列問題】

給定正整數序列 \(x_1 \dots, x_n\)

  1. 計算其最長不下降子序列的長度 \(s\)
  2. 如果每個元素只允許使用一次,計算從給定的序列中最多可取出多少個長度為 \(s\) 的不下降子序列。
  3. 如果允許在取出的序列中多次使用 \(x_1\)\(x_n\)(其他元素仍然只允許使用一次),則從給定序列中最多可取出多少個不同的長度為 \(s\) 的不下降子序列。

這裡還是沒有太吃透,有點朦朧

/*
 * @Author: zhl
 * @Date: 2020-10-21 15:19:08
 */
#include<bits/stdc++.h>
#define rep(i,a,b) for(int i = a;i <= b;i++)
#define repE(i,u) for(int i = head[u]; ~i; i = E[i].next)
using namespace std;

const int N = 1e4 + 10, M = 1e5 + 10, inf = 1e9;
struct Edge {
	int to, next, flow;
}E[M << 1];
int head[N], tot;
void addEdge(int from, int to, int flow) {
	E[tot] = Edge{ to,head[from],flow };
	head[from] = tot++;
	E[tot] = Edge{ from,head[to],0 };
	head[to] = tot++;
}

int n, m, s, t, P_nums;
int dis[N], cur[N];
bool bfs() {
	for (int i = 0; i <= P_nums; i++)dis[i] = -1;
	queue<int>Q;
	Q.push(s); cur[s] = head[s]; dis[s] = 0;
	while (!Q.empty()) {
		int u = Q.front(); Q.pop();
		repE(i, u) {
			int v = E[i].to;
			if (dis[v] == -1 and E[i].flow) {
				dis[v] = dis[u] + 1;
				cur[v] = head[v];
				Q.push(v);
				if (v == t)return true;
			}
		}
	}
	return false;
}

int dfs(int u, int limit) {
	if (u == t)return limit;
	int k, res = 0;

	for (int i = cur[u]; ~i and res < limit; i = E[i].next) {
		cur[u] = i;
		int v = E[i].to;
		if (dis[v] == dis[u] + 1 and E[i].flow) {
			k = dfs(v, min(limit, E[i].flow));
			if (!k)dis[v] = -1;
			E[i].flow -= k; E[i ^ 1].flow += k;
			limit -= k; res += k;
		}
	}
	return res;
}

int Dinic() {
	int res = 0;
	while (bfs())res += dfs(s, inf);
	return res;
}

int f[N], A[N];
int main() {
	scanf("%d", &n);
	P_nums = 2 * n + 1;
	s = 0, t = 2 * n + 1;
	memset(head, -1, sizeof(int) * (2 * n + 100));
    
	rep(i, 1, n)scanf("%d", A + i);
    if(n == 1){
        puts("1");puts("1");puts("1");
        return 0;
    }
	for (int i = n; i >= 1; i--) {
		f[i] = 1;
		for (int j = n; j > i; j--) {
			if (A[i] <= A[j]) {
				f[i] = max(f[i], f[j] + 1);
			}
		}
	}

	int ans = 0;
	rep(i, 1, n)ans = max(ans, f[i]);
	printf("%d\n", ans);

	rep(i, 1, n)addEdge(i, i + n, 1);
	rep(i, 1, n) {
		if (f[i] == ans)addEdge(0, i, 1);
		if (f[i] == 1)addEdge(i + n, 2 * n + 1, 1);
	}
	rep(i, 1, n) {
		rep(j, i + 1, n) {
			if (A[j] >= A[i] and f[j] + 1 == f[i]) {
				addEdge(i + n, j, 1);
			}
		}
	}
	printf("%d\n", Dinic());

	memset(head, -1, sizeof(int) * (2 * n + 100));
	tot = 0;

	rep(i, 1, n)addEdge(i, i + n, 1);
	rep(i, 1, n) {
		if (f[i] == ans)addEdge(0, i, 1);
		if (f[i] == 1)addEdge(i + n, 2 * n + 1, 1);
	}
	rep(i, 1, n) {
		rep(j, i + 1, n) {
			if (A[j] >= A[i] and f[j] + 1 == f[i]) {
				addEdge(i + n, j, 1);
			}
		}
	}
	if(f[1] == ans){addEdge(0, 1, inf); addEdge(1, 1 + n, inf);}
	if(f[n] == 1){addEdge(n, 2 * n, inf); addEdge(2 * n, 2 * n + 1, inf);}
	printf("%d\n", Dinic());

}

企鵝旅行

在南極附近的某個地方,一些企鵝正站在一些浮冰上。

作為群居動物,企鵝們喜歡聚在一起,因此,它們想在同一塊浮冰上會合。

企鵝們不想淋溼自己,所以它們只能利用自己有限的跳躍能力,在一塊塊浮冰之間跳躍移動,從而聚在一起。

但是,最近的溫度很高,浮冰上也有了裂紋。

每當企鵝在一塊浮冰上發力跳到另一塊浮冰上時,起跳的浮冰都會遭到破壞,落點的浮冰並不會因此受到影響。

當浮冰被破壞到一定程度時,浮冰就會消失。

現在已知每塊浮冰可以承受的具體起跳次數。

請幫助企鵝找出它們可以會合的所有浮冰。

/*
 * @Author: zhl
 * @Date: 2020-10-21 16:43:14
 */

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i = a;i <= b;i++)
#define repE(i,u) for(int i = head[u];~i;i = E[i].next)
using namespace std;

const int N = 2e2 + 10, M = 2e6 + 10, inf = 1e9;
struct Edge {
	int to, flow, next;
}E[M << 1];
int head[N], tot;
void addEdge(int from, int to, int w) {
	E[tot] = Edge{ to,w,head[from] };
	head[from] = tot++;
	E[tot] = Edge{ from,0,head[to] };
	head[to] = tot++;
}

int n, m, s, t, P_nums;//圖中總點數[0-P_nums]
queue<int>Q;
int dis[N], cur[N];
bool bfs() {
	rep(i, 0, P_nums)dis[i] = -1;
	while (!Q.empty())Q.pop();
	Q.push(s); dis[s] = 0; cur[s] = head[s];
	while (!Q.empty()) {
		int u = Q.front(); Q.pop();
		repE(i, u) {
			int v = E[i].to;
			if (dis[v] == -1 and E[i].flow) {
				cur[v] = head[v];
				dis[v] = dis[u] + 1;
				Q.push(v);
				if (v == t)return true;
			}
		}
	}
	return false;
}

int dfs(int u, int limit) {
	if (u == t)return limit;
	int k, res = 0;
	for (int i = cur[u]; ~i and res < limit; i = E[i].next) {
		int v = E[i].to;
		cur[u] = i;
		if (dis[v] == dis[u] + 1 and E[i].flow) {
			k = dfs(v, min(limit - res, E[i].flow));
			if (k == 0)dis[v] = -1;
			E[i].flow -= k; E[i ^ 1].flow += k;
			res += k;
		}
	}
	return res;
}

int Dinic() {
	int res = 0, f;
	while (bfs())while (f = dfs(s, inf)) res += f;
	return res;
}

int T; double r;
int x[N], y[N], now[N], G[N];
int cal_dis(int i, int j) {
	return 1.0 * (x[i] - x[j]) * (x[i] - x[j]) + (y[i] - y[j]) * (y[i] - y[j]);
}

int main() {
	scanf("%d", &T);
	while (T--) {
		scanf("%d%lf", &n, &r);
		memset(head, -1, sizeof head);
		tot = 0;
		P_nums = 2 * n;

		int sum = 0;
		rep(i, 1, n) {
			scanf("%d%d%d%d", x + i, y + i, now + i, G + i); sum += now[i];
			addEdge(i, i + n, G[i]); addEdge(0, i, now[i]);
		}
		rep(i, 1, n)
			rep(j, i + 1, n) {
			//if (i == j)continue;
			double d = cal_dis(i, j);
			if (1.0 * d <= r * r) addEdge(i + n, j, inf), addEdge(j + n, i, inf);
		}

		int cnt = 0;
		rep(i, 1, n) {
			s = 0, t = i;
			for (int i = 0; i < tot; i += 2) { E[i].flow += E[i ^ 1].flow; E[i ^ 1].flow = 0; }
			if (Dinic() == sum) {
				if (cnt)printf(" ");
				printf("%d", i - 1);
				cnt++;
			}
		}
		if (!cnt)printf("-1");
		puts("");
	}
}

這裡有個很神奇的操作

for (int i = 0; i < tot; i += 2) { 
	E[i].flow += E[i ^ 1].flow; 
	E[i ^ 1].flow = 0; 
}

可以重置邊的狀態,不需要重新建邊

看完題目,我很熟練的建立了一個這樣的毒瘤圖

於是我快樂的 AC 了它。

int main() {
	scanf("%d%d", &m, &n);
	memset(head, -1, sizeof head);
	rep(i, 1, m) { scanf("%d", &s); addEdge(0, i, s); }

	s = 0, t = n * (m + 1) + 1;

	rep(i, 1, n) {
		int a, x;
		scanf("%d", &a);
		int base = (m + 1) * (i - 1);
		int r = (m + 1) * i;
		rep(j, 1, a) {
			scanf("%d", &x);
			addEdge(base + x, r, inf);
			addEdge(r, base + x, inf);
		}
		scanf("%d", &x); addEdge(r, t, x);
		if (i < n) {
			rep(j, 1, m)addEdge(base + j, base + j + m + 1, inf);
		}
	}
	printf("%d\n", Dinic());
}

但是,我眉頭一皺,發現事情並不簡單

\(5700ms\) ...

算了一下,大概有 \(n*(m+1)+1\) 這麼多個點, \(1e5\) ... 邊開了 \(1e6\) 才過

雖然說複雜度很寬鬆 , \(O(n^2m)\) ..

於是我看了一眼題解, 只有 \(n\) 個點

“在面對網路流問題時,如果一時想不出很好的構圖方法,不如先構造一個最直觀,或者說最“硬來”的模型,然後再用合併節點和邊的方法來簡直化這個模型。經過簡化以後,好的構圖思路自然就會湧現出來了。這是解決網路流問題的一個好方法。”

很顯然我這個就是最原始的。

實際上這個圖是可以合併的

可以把每一層的 $m + 1 $ 個點進行合併成一個點,記錄下每個