[TOC]

0 前言&小技巧

一些STL函数

__builtin_popcount(i): 求 i 的二进制中1的个数

**round(double x): ** 四舍五入

**ceil(double x): ** 向上取整

**floor(double x): ** 向下取整

**__gcd(int a, int b): ** 内置 gcd

1 基础算法&贪心

1.1 二分查找

1.1.1 整数二分

数组a中第一个 ==(⇐) t的数

// 数组下标从0开始, 从1则 l = 0, r = n + 1
int l = -1, r = n;
while(l + 1 != r)
{
	int mid = l + r >> 1;
	if(a[mid] < t)
		l = mid;
	else 
		r = mid;
}
// 当所有数都小于t时, r会为n, 当没有等于t的数时,r为第一个大于t的数, 当所有数都大于t时, r为第一个数
if(r != n && a[r] == t) return r;
else return -1;

数组a中最后一个 ==(>=) t的数

int l = -1, r = n;
while(l + 1 != r)
{
	int mid = l + r >> 1;
	if(a[mid] <= t)
		l = mid;
	else
		r = mid;
}
// 当所有数都小于t时或没有数等于t时, l为 从后往前第一个小于t的数, 当所有数都大于t时, l 为 -1
if(l != -1 && a[l] == t) return l;
else return -1;

1.1.2 浮点二分

给定一个浮点数 n,求它的三次方根。

int main()
{
    double n;
    cin >> n;
    double l = -1000, r = 1000;
    while(r - l > 1e-7)
    {
        double mid = (l+r) / 2.0;
        if(mid * mid * mid >= n) r = mid;
        else l = mid;
    }
    printf("%.6lf",l);
    return 0;
}

1.2 区间选点

给定 N 个闭区间 [ai,bi],请你在数轴上选择尽量少的点,使得每个区间内至少包含一个选出的点。

输出选择的点的最小数量。

位于区间端点上的点也算作区间内。

typedef pair<int,int> PII;
PII s[N];
int n;
int main()
{
	cin >> n;
	for(int i = 0; i < n;i ++)
	{
		int a,b;
		cin >> a >> b;
		s[i] = {a,b};
	}
	sort(s, s + n);
	int cnt = 0, L = -2e9, R = -2e9;
	for(int i = 0; i < n;i ++)
	{
		int l = s[i].first, r = s[i].second;
		if(l > R)
		{
			cnt++;
			L = l, R = r;
		}
		else R = min(R, r);
	}
	cout << cnt << endl;
	return 0;
}

1.3 区间分组

给定 N 个闭区间 [ai,bi],请你将这些区间分成若干组,使得每组内部的区间两两之间(包括端点)没有交集,并使得组数尽可能小。

输出最小组数。

对于区间i, 当 i.l 小于等于 所有分组中最小的那个 max_r, 就肯定与所有分组重叠, 需要新增一个分组。 若大于则说明 可以加入到 最小的那个分组里, 如果 i.r 大于 max_r 则更新 一下。

struct Range{
    int l, r;
}range[N];
 
int n;
struct cmpFunctor{
    inline bool operator () (const Range &a, const Range &b)
    {
        return a.l < b.l;
    }
};
 
int main()
{
    cin >> n;
    for(int i = 0; i < n; i++)
    {
        int l,r;
        cin >> l >> r;
        range[i] = {l,r};
    }
    sort(range, range + n,cmpFunctor());
    
    priority_queue<int, vector<int>, greater<int>> q;
    for(int i = 0; i < n; i++)
    {
        if(q.empty() || q.top() >= range[i].l)
            q.push(range[i].r);
        else
        {
            q.pop();
            q.push(range[i].r);
        }
    }
    cout << q.size() << endl;
    return 0;
}

1.4 区间覆盖

给定 N 个闭区间 [ai,bi] 以及一个线段区间 [s,t],请你选择尽量少的区间,将指定线段区间完全覆盖。

输出最少区间数,如果无法完全覆盖则输出 −1。

对于 i.l <= st 的区间, 选择右端点最大的 max_r 然后将右端点重置为 st, 接着重复选择。

当 max_r 始终小于 ed 时, 说明无法覆盖, 若超过ed则直接退出提交答案

故按左端点排序, 遍历所有区间 从i开始, 往后找最后一个 range[i].l <= st 的区间, 期间一直更新 max_r 判断 max_r 是否小于 st, 若小于说明没有区间能覆盖开头 判断 max_r 是否大于 ed, 若大于说明已经覆盖结尾

struct Range{
    int l, r;
}range[N];
int n;
int s,t;
struct cmpFunction
{
    inline bool operator () (const Range&a, const Range&b)
    {
        return a.l < b.l;
    }
};
 
int main()
{
    cin >> s >> t >> n;
    for(int i = 0; i < n;i ++)
    {
        int l,r;
        cin >> l >> r;
        range[i] = {l,r};
    }
    sort(range, range + n, cmpFunction());
    
    int res = 0;
    bool flag = false;
    for(int i = 0; i < n;i ++)
    {
        int j = i, r = -2e9;
        while(j < n && range[j].l <= s)
        {
            r = max(r, range[j].r);
            j++;
        }
        if(r < s)
        {
            res = -1;
            break;
        }
        
        res++;
        if(r >= t)
        {
            flag = true;
            break;
        }
        s = r;
        i = j - 1;
        
    }
    if(!flag) res = - 1;
    cout << res << endl;
    return 0;
}

1.5 Huffman树合并问题

Huffman树就是管合并问题, image-20230503163819568 问题的本质是构建一个完全二叉树, 总权值最小 3a + 3b + 3c + 3d + 2e + 2f

显然, 深度越深的点会被计算更多次。 那么 1 相比 8 就更适合放在深度深的点上, 对于总权值的贡献更小。

int main()
{
    int n;
    cin >> n;
    priority_queue<int, vector<int>, greater<int>> q;
    while (n -- )
    {
        int x;
        cin >> x;
        q.push(x);
    }
    int res = 0;
    while(q.size() > 1)
    {
        int a = q.top(); q.pop();
        int b = q.top(); q.pop();
        res += a + b;
        q.push(a + b);
    }
    cout << res << endl;
    return 0;
}

1.6 绝对值不等式

在一条数轴上有 N 家商店,它们的坐标分别为 A1∼AN。

现在需要在数轴上建立一家货仓,每天清晨,从货仓到每家商店都要运送一车商品。

为了提高效率,求把货仓建在何处,可以使得货仓到每家商店的距离之和最小。

image-20230503164008882

就建到他们的中点即可, 若为总共为奇数个就是n/2位置的, 若为偶数则中间点也是 n/2 就行。

int n;
int a[N];
 
int main()
{
    cin >> n;
    for(int i = 1; i <= n;i ++) scanf("%d", &a[i]);
    sort(a + 1, a + n + 1);
    int mid = a[n / 2 + 1], res = 0;
    for(int i = 1; i <= n; i++) res += abs(a[i] - mid);
    cout << res << endl;
    return 0;
}

1.7 高精度

1.7.1 数组高精度乘加比较

void add(LL a[], LL b[])
{
    static LL c[M];
    mem(c, 0);
    for (int i = 0, t = 0; i < M; i++)
    {
        t += a[i] + b[i];
        c[i] = t % 10;
        t /= 10;
    }
    memcpy(a, c, sizeof c);
}
void mul(LL a[], LL b)
{
    static LL c[M];
    mem(c, 0);
    LL t = 0;
    for (int i = 0; i < M; i++)
    {
        t += a[i] * b;
        c[i] = t % 10;
        t /= 10;
    }
    memcpy(a, c, sizeof c);
}
 
int cmp(LL a[], LL b[])
{
    for (int i = M - 1; i >= 0; i--)
        if (a[i] > b[i])
            return 1;
        else if (a[i] < b[i])
            return -1;
    return 0;
}
 
void print(LL a[])
{
    int k = M - 1;
    while (k && !a[k])
        k--;
    for (k; k >= 0; k--)
        cout << a[k];
    cout << endl;
}

1.7.2 vector 加减乘除比较

bool check(vint a, vint b)
{
    if(a.size() < b.size()) return false;
    if(a.size() == b.size())
    {
        for(int i = a.size() - 1; i >= 0; i--)
            if(a[i] > b[i]) return true;
            else if(a[i] < b[i]) return false;
    }
    return true;
}
 
vector<int> add(vector<int>a,vector<int>b)
{
    vector<int> c;
    int t = 0;
    for(int i = 0;i < a.size() || i < b.size(); i++)
    {
        if(i < a.size()) t += a[i];
        if(i < b.size()) t += b[i];
        c.push_back(t % 10);
        t /= 10;
    }
    if(t) c.push_back(t);
    return c;
}
 
vint mul(vint &a, int b)
{
    vint c;
    if(b == 0) 
    {
        c.push_back(0);
        return c;
    }
    int t = 0;
    for(int i = 0; i < a.size(); i++)
    {
        t += a[i] * b;
        c.push_back(t % 10);
        t /= 10;
    }
    if(t) c.push_back(t);
    return c;
}
 
vint div(vint& a, int b, int& r)
{
    vint c;
    for (int i = a.size() - 1; i >= 0; i--)
    {
        r = r * 10 + a[i];
        c.push_back(r / b);
        r = r%b;
    }
    reverse(c.begin(), c.end());
    while (c.size() > 1 && c.back() == 0) c.pop_back();
    return c;
}
 
vint sub(vint a, vint b)
{
    vint c;
    int t = 0;
    for(int i = 0; i < a.size(); i++)
    {
        t += a[i];
        if(i < b.size()) t -= b[i];
        c.push_back((t + 10) % 10);
        if(t < 0) t = -1;
        else t = 0;
    }
    while(c.size() > 1 && c.back() == 0) c.pop_back();
    return c;
}

1.8 固定数组求第K个数 O(log n)

int quick_sort(int l, int r, int k)
{
    if(l >= r) return a[l];
    int i = l - 1, j = r + 1, x = a[l + r >> 1];
    while(i < j)
    {
        while(a[++i] < x);
        while(a[--j] > x);
        if(i < j) swap(a[i], a[j]);
    }
    
    int s1 = j - l + 1;
    if(s1 < k)
        return quick_sort(j + 1, r, k - s1);
    else 
        return quick_sort(l, j, k);
}

1.9 求逆序对个数 O(log n)

long long merge_sort(int l, int r)
{
    if (l >= r)
        return 0;
    int mid = l + r >> 1;
    long long res = merge_sort(l, mid) + merge_sort(mid + 1, r);
 
    int i = l, j = mid + 1, k = 0;
    while (i <= mid && j <= r)
        if (a[i] <= a[j])
            temp[k++] = a[i++];
        else
        {
            res += mid - i + 1;
            temp[k++] = a[j++];
        }
    while (i <= mid)
        temp[k++] = a[i++];
    while (j <= r)
        temp[k++] = a[j++];
 
    for (i = l, j = 0; i <= r; i++, j++)
        a[i] = temp[j];
    return res;
}

1.10 差分矩阵

const int N = 1010;
int n, m, q;
int S[N][N],a[N][N];
 
void insert(int x1,int y1,int x2,int y2,int c)
{
    S[x1][y1]+=c;
    S[x2+1][y2+1]+=c;
    S[x1][y2+1]-=c;
    S[x2+1][y1]-=c;
}
 
 
 
int main()
{
    scanf("%d%d%d",&n,&m,&q);
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
        {
            scanf("%d",&a[i][j]);
        }
    }
    while(q--)
    {
        int x1,y1,x2,y2,c;
        scanf("%d%d%d%d%d",&x1,&y1,&x2,&y2,&c);
        insert(x1,y1,x2,y2,c);
    }
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
        {
            S[i][j]+=S[i-1][j]+S[i][j-1]-S[i-1][j-1];
            printf("%d ",a[i][j]+S[i][j]);
        }
        printf("\n");
    }
 
    return 0;
}
 

1.11 最长连续不重复子序列

给定一个长度为 n 的整数序列,请找出最长的不包含重复的数的连续区间,输出它的长度。

每个数都在 0~1e5

const int N = 1e6;
int a[N];
int st[N];
 
int main()
{
    int n;
    cin >> n;
    for(int i = 0; i < n;i ++) cin >> a[i];
    int res = 0;
    for(int i = 0, j = 0; i < n; i++)
    {
        int num = a[i];
        st[num]++;
        while(st[num] > 1)
        {
            st[a[j]]--;
            j++;
        }
        res = max(res, i - j + 1);
    }
    cout << res << endl;
    return 0;
}
 

1.12 满足 a[i] + b[i] = x 的数对 (i,j)

#include <iostream>
const int N = 1e6;
int a[N];
int b[N];
 
int main() {
    int n,m,c;
    scanf("%d%d%d",&n,&m,&c);
    for(int i = 0; i < n;i ++) scanf("%d",&a[i]);
    for(int i = 0; i < m; i++) scanf("%d",&b[i]);
    
    for(int i = 0, j = m - 1; i < n; i++) {
        while(j >= 0 && a[i] + b[j] > c) j--;
        if(a[i] + b[j] == c) {
            printf("%d %d", i, j);
            break;
        }
    }
    
    return 0;
}

2 搜索

2.1 BFS

什么时候适合用宽搜?

  1. 求最小
  2. 基迭代, 不会爆栈。 层数很深但节点个数不多的时候。

2.1.1 A-star 求第K短路

给定一张 个点(编号 ), 条边的有向图,求从起点 到终点 的第 短路的长度,路径允许重复经过点或边。

注意: 每条最短路中至少要包含一条边。


一个显而易见的解法是用朴素BFS, 第一次枚举到终点时就是第一短路, 第二次就是第二最短路, 经过第k次时就是第k最短路。 再看一下时间复杂度, 个点, 条边, 假设每个点10条边, 即 , 肯定超时。这么大的范围必须通过更优化的方式解决。

BFS的优化有双向BFS和A-star, 双向显然也会超时, 一样结果。 那么只有A-star了, 这次不是只求一次最短路, 故需要把只入队最小权值改为能入队都入队, 不过出队仍然是最小权值出队, 保证是最短路。 而预期函数需要满足小于等于真实值, 很难通过计算得出, 这里直接以终点为起点做一次整个图的dijkstra, 求出每个点的最短路径, 以该值作为预期值。

最后就是在第k次时输出结果即可。

void add(int h[], int a ,int b, int c)
{
    e[idx] = b,w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
 
void dijkstra()
{
    priority_queue<PII, vector<PII>, greater<PII>> q;
    q.push({0,T});
    memset(dist, 0x3f, sizeof dist);
    dist[T] = 0;
    while(q.size())
    {
        auto t = q.top();
        q.pop();
        int ver = t.y;
        if(st[ver]) continue;
        st[ver] = true;
        
        for(int i = hr[ver]; ~i; i = ne[i])
        {
            int j = e[i];
            if(dist[j] > dist[ver] + w[i])
            {
                dist[j] = dist[ver] + w[i];
                if(!st[j])
                    q.push({dist[j],j});
            }
        }
    }
}
 
int astar()
{
    priority_queue<PIII,vector<PIII>, greater<PIII>> q;
    q.push({dist[S], {0, S}});
    int cnt = 0;
    if(dist[S] ==  0x3f3f3f3f) return -1;
    while(q.size())
    {
        auto t = q.top();
        q.pop();
        int ver = t.y.y, distance = t.y.x;
        if(ver == T) cnt++;
        if(cnt >= K) return distance;
        
        for(int i = h[ver]; ~i; i = ne[i])
        {
            int j = e[i];
            q.push({dist[j] + distance + w[i], {distance + w[i], j}});
        }
    }
    
    return -1;
}
int main()
{
    memset(h, -1, sizeof h);
    memset(hr, -1, sizeof hr);
    cin >> n >> m;
    while(m--)
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(h, a, b, c);
        add(hr, b, a, c);
    }
    cin >> S >> T >> K;
    if(S == T) K++;
    
    dijkstra();
    
 
    cout << astar() << endl;
    return 0;
}

2.1.2 Flood Fill 求山峰与山谷个数

给定一个 的网格状地图,每个方格 有一个高度 。如果两个方格有公共顶点,则它们是相邻的。

定义山峰和山谷如下:

  • 均由地图上的一个连通块组成;
  • 所有方格高度都相同;
  • 周围的方格(即不属于山峰或山谷但与山峰或山谷相邻的格子)高度均大于山谷的高度,或小于山峰的高度。

求地图内山峰和山谷的数量。特别地,如果整个地图方格的高度均相同,则整个地图既是一个山谷,也是一个山峰。

image-20230503164628876

思路:

依然是一个点周围8个点都算连通, 这里需要找山峰的数量和山谷的数量。 山峰是指该点的权值大于周围点的权值的连通点集是一个山峰。 山谷同理, 是该点权值小于周围点权值的连通点集是一个山谷。

同样声明st数组记录当前点是否被枚举过, 先循环遍历所有点, 遇到未枚举过的点就进行BFS泛洪标记与当前值相同的连通点集, 并判断点集周围是否存在大于的点或者小于的点。

如果周围不存在小于当前权值的点, 说明当前点集为山谷。 若周围不存在大于当前取值的点, 说明当前点集为山峰。

若既不存在大于也不存在小于, 说明该图权值都相等, 即是山谷也是山峰。

const int N = 1e3 + 10;
int g[N][N];
int n;
bool st[N][N];
int res_u, res_n; // 山谷与山峰
typedef pair<int, int> PII;
PII q[N * N];
 
void bfs(int xx, int yy, bool &highter, bool &lower)
{
    int hh = 0, tt = 0;
    q[0] = {xx, yy};
    st[xx][yy] = true;
    while (hh <= tt)
    {
        auto t = q[hh++];
        for (int i = t.first - 1; i <= t.first + 1; i++)
            for (int j = t.second - 1; j <= t.second + 1; j++)
            {
                if (i < 1 || i > n || j < 1 || j > n)
                    continue;
                if (g[i][j] != g[t.first][t.second])
                {
                    if (g[i][j] > g[t.first][t.second])
                        highter = true;
                    else
                        lower = true;
                }
                else if (!st[i][j])
                {
                    q[++tt] = {i, j};
                    st[i][j] = true;
                }
            }
    }
}
 
int main()
{
    cin >> n;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            cin >> g[i][j];
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
        {
            if (st[i][j])
                continue;
            bool higher = false, lower = false;
            bfs(i, j, higher, lower);
            if (!higher)
                res_n++;
            if (!lower)
                res_u++;
        }
    cout << res_n << " " << res_u << endl;
 
    return 0;
}

2.1.3 双向广搜

给一个串A, 和6个变换规则, 求变化到B串要用的最少步数。

应用一个规则时, 需要枚举串A, 找到可以应用的点, 然后应用。因此, 最坏情况下, 长度为20的A串, 每个位置都能应用6个规则, 搜10次的话为 。不过肯定不会都能应用, 可以只考虑规则数, 时间复杂度为 , 这同样还是太高了。

需要进行优化, 这里采用双向BFS的方法, 将时间复杂度优化到 , 很舒服。 双向BFS过程为: 定义俩队列qa qb, qa为从起点开始的BFS, qb为从终点开始的BFS。 每次扩展时优先扩展队列长度小的。

该题扩展时需要枚举当前字符串所有位置, 然后再枚举规则进行应用, 若新得出的串未被该方向上的哈希表标记, 则加入队列并标记。 若已被另一个方向的哈希表标记, 说明搜索相遇, 当前路径就是最小路径, 输出答案即可。 若扩展时有一个队列为空, 说明起点和中间不存在通路, 直接返回无解。

const int N = 6;
int n;
string a[N], b[N];
int temp;
 
int extend(queue<string> &q, unordered_map<string, int> &da, unordered_map<string,int> &db, string a[], string b[])
{
    string t = q.front();
    q.pop();
    
    for(int i = 0; i < t.size(); i++)
        for(int j = 0; j < 6; j++)
        {
            if(t.substr(i, a[j].size()) == a[j])
            {
                string state = t.substr(0,i) + b[j] + t.substr(i + a[j].size());
                
                if(db.count(state)) return da[t] + db[state] + 1;
                if(da.count(state)) continue;
                q.push(state);
                da[state] = da[t] + 1;
            }
        }
    return 11;
}
 
int bfs(string A, string B)
{
    if(A == B) return 0;
    queue<string> qa,qb;
    unordered_map<string, int> da, db;
    qa.push(A), da[A] = 0;
    qb.push(B), db[B] = 0;
    while(qa.size() && qb.size())
    {
        int t = 11;
        if(qa.size() <= qb.size()) t = extend(qa, da, db, a, b);
        else t = extend(qb, db, da, b, a);
        if(t <= 10) return t;
    }
    return 11;
}
 
 
int main()
{
    string A, B;
    cin >> A >> B;
    while(cin >> a[n] >> b[n])n++;
    int step = bfs(A,B);
    if(step > 10) puts("NO ANSWER!");
    else cout << step << endl;
    return 0;
}

2.1.4 双端队列与位置坐标—地图坐标的映射

从一个点走到相邻的另一个点有两种情况, 不旋转, 旋转, 这两条边的边权分别为0, 1。 故我们要求的就是从起点到终点的一个无向图最短路问题。

求之前需要先找出所有不可能抵达的点, 避免在计算路径时把他们的权值也算上。 这里的话简单枚举可以归纳出来就是坐标和为偶数的节点都是不可抵达的节点。

只包含01边权的最短路可以用双端队列来做。 双端队列就是把扩展队头元素所得到的元素, 本来是要统一插到队尾, 这里加个判断, 若边权为0则插到队头, 若边权为1则插到队尾。其余都和BFS一样。

扩展队头时需要确认两点:

  • 扩展点的坐标
  • 该路径的权值

image-20230503164911986

显然对于点(1,2)只能通过斜着走到其他点, 故dx,dy为:

// 左上 右上 左下 右下
dx[] = {-1,-1,1,1};
dy[] = {-1,1,-1,1};

而从一个点走到另一个点时, 还需要判断两点之间的线是否需要旋转, 这里用ix, iy计算出两点之间的线对应在 线数组 中的位置, 然后与正常不需要旋转时的符号进行对比, 若相同则权值为0, 不同则权值为1。

// 左上 右上 左下 右下
ix[] = {-1, -1, 0, 0};
iy[] = {-1, 0, -1, 0};
char cs[] = {'\\', '/', '/', '\\'};

在BFS过程中, 若扩展的点已经被搜过, 不能直接丢弃, 有可能之前使用边权为1更新, 这里是用边权为0更新。 过程可以看做一个简化版的dijkstra。

const int N = 510;
typedef pair<int, int> PII;
char g[N][N];
int n, m;
int dist[N][N];
bool st[N][N];
int dx[] = {-1, -1, 1, 1}, dy[] = {-1, 1, -1, 1};
int ix[] = {-1, -1, 0, 0}, iy[] = {-1, 0, -1, 0};
char cs[] = "\\//\\";
 
int bfs()
{
    deque<PII> q;
    memset(dist, 0x3f, sizeof dist);
    memset(st, false, sizeof st);
    dist[0][0] = 0;
    q.push_back({0, 0});
    while (q.size())
    {
        auto t = q.front();
        q.pop_front();
        int x = t.first, y = t.second;
        if (x == n && y == m)
            return dist[x][y];
        if (st[x][y])
            continue;
        st[x][y] = true;
 
        for (int i = 0; i < 4; i++)
        {
            int xa = x + dx[i], yb = y + dy[i];
            if (xa < 0 || xa > n || yb < 0 || yb > m)
                continue;
            int ga = x + ix[i], gb = y + iy[i];
            int w = g[ga][gb] != cs[i];
            if (dist[xa][yb] > dist[x][y] + w)
            {
                dist[xa][yb] = dist[x][y] + w;
                if (!w)
                    q.push_front({xa, yb});
                else
                    q.push_back({xa, yb});
            }
        }
    }
    return -1;
}
 
int main()
{
    int T;
    cin >> T;
    while (T--)
    {
        cin >> n >> m;
        for (int i = 0; i < n; i++)
            cin >> g[i];
 
        if ((n + m) % 2)
            cout << "NO SOLUTION" << endl;
        else
            cout << bfs() << endl;
    }
 
    return 0;
}

2.1.5 多源BFS与魔板操控模拟

这是一张有 个大小相同的格子的魔板:

1 2 3 4
8 7 6 5

这里提供三种基本操作,分别用大写字母 A,B,C 来表示(可以通过这些操作改变魔板的状态):

  • A:交换上下两行;
  • B:将最右边的一列插入最左边;
  • C:魔板中央作顺时针旋转。

你要编程计算用最少的基本操作完成基本状态到特殊状态的转换,输出基本操作序列。

类似于BFS求方案, 从终末状态逆向泛洪, 记录prev数组指向上一个和当前走的操作。

存状态通常使用哈希法, 把整个状态用一个最小整数存下来。 可以自己手写哈希, 或者用康拓展开, 或者用STL的map/unordered_map

char g[2][4];
unordered_map<string, int> dist;
unordered_map<string, pair<int, string>> pre;
queue<string> q;
string start, endstate;
 
void set(string s)
{
    for (int i = 0; i < 4; i++)
        g[0][i] = s[i];
    for (int i = 3, j = 4; i >= 0; i--, j++)
        g[1][i] = s[j];
}
 
string get()
{
    string s;
    for (int i = 0; i < 4; i++)
        s += g[0][i];
    for (int i = 3; i >= 0; i--)
        s += g[1][i];
    return s;
}
 
string move0(string t)
{
    set(t);
    for (int i = 0; i < 4; i++)
        swap(g[0][i], g[1][i]);
    return get();
}
string move1(string t)
{
    set(t);
    char v0 = g[0][3], v1 = g[1][3];
    for (int i = 3; i >= 0; i--)
        g[0][i] = g[0][i - 1], g[1][i] = g[1][i - 1];
    g[0][0] = v0, g[1][0] = v1;
    return get();
}
string move2(string t)
{
    set(t);
    char v0 = g[0][1];
    g[0][1] = g[1][1];
    g[1][1] = g[1][2];
    g[1][2] = g[0][2];
    g[0][2] = v0;
    return get();
}
 
void bfs(string start, string end)
{
    q.push(end);
    dist[end] = 0;
    while (!q.empty())
    {
        auto t = q.front();
        q.pop();
 
        string m[3];
        m[0] = move0(t);
        m[1] = move1(t);
        m[2] = move2(t);
        for (int i = 0; i < 3; i++)
        {
            string str = m[i];
            if (dist.count(str))
                continue;
            dist[str] = dist[t] + 1;
            pre[str] = {char(i + 'A'), t};
            if (str == start)
                return;
            q.push(str);
        }
    }
}
 
int main()
{
    for (int i = 0; i < 8; i++)
    {
        int t;
        cin >> t;
        start += char(t + '0');
        endstate += char(i + '1');
    }
 
    bfs(start, endstate);
 
    cout << dist[start] << endl;
 
    string res;
    while (start != endstate)
    {
        res += pre[start].first;
        start = pre[start].second;
    }
    rev(res);
    if (res.size())
        cout << res << endl;
    return 0;
}

2.1.6 BFS最短路求方案路径

之前的思路是用二维数组记录走到每一点所用步数, 根据BFS找到结果就退出的特性, 只存在一条路径从起点到终点满足递增, 可以像DP求方案那样逆推一遍得到。

逆推的操作可以使用DFS。

这里是把用来记录是否走过的数组bool st[][]扩展为 pair<int,int> prev[][], 即每个格子是从哪个格子走过来的。

const int N = 6;
int g[N][N];
typedef pair<int, int> PII;
PII prevs[N][N];
PII q[N * N];
int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};
 
void bfs(int xx, int yy)
{
    int hh = 0, tt = 0;
    q[0] = {xx, yy};
    prevs[xx][yy] = {-1, -1};
    while (hh <= tt)
    {
        auto t = q[hh++];
        int x = t.first, y = t.second;
        if (x == 1 && y == 1)
            return;
        for (int i = 0; i < 4; i++)
        {
            int a = x + dx[i], b = y + dy[i];
            if (a < 1 || a > 5 || b < 1 || b > 5 || g[a][b])
                continue;
            if (prevs[a][b].first == 0 && prevs[a][b].second == 0)
            {
                q[++tt] = {a, b};
                prevs[a][b] = {x, y};
            }
        }
    }
}
 
void print_ans(int x, int y)
{
    if (x == -1 && y == -1)
        return;
    print_ans(prevs[x][y].first, prevs[x][y].second);
    printf("(%d, %d)\n", x - 1, y - 1);
}
 
int main()
{
    for (int i = 1; i <= 5; i++)
    {
        for (int j = 1; j <= 5; j++)
        {
            cin >> g[i][j];
        }
    }
    bfs(5, 5);
    PII end(1, 1);
    while (true)
    {
        printf("(%d, %d)\n", end.first - 1, end.second - 1);
        if (end.first == 5 && end.second == 5)
            break;
        end = prevs[end.first][end.second];
    }
    return 0;
}

2.2 DFS

2.2.1 IDA-star

在迭代加深的基础上, 加一条剪枝: 在每一个点搜的同时, 计算损失函数, 若当前点找到答案所需最小步数与当前步数相加一定大于当前的max_depth, 则直接返回。

跟生日蛋糕一题中的v+minv[u] <= m有点类似。

A-star 类似于MC在矿洞中, 拿着一个只会滴滴响的矿物探测器, 往哪个方向走响的更频繁往哪走是对的

IDA-star 是在食物有限情况下, 只能给你走几步路, 还有一个矿物探测器, 不过能更精确得估计出接下来要走的步数。

image-20230503165730741

一个#号序列, 可以对其做8种操作, 使得中间8个数相同, 要求找到最小操作数。

考虑爆搜。 搜索顺序: u为当前操作步数, depth为最大深度, last为上一步操作记录。 每次可以对其操作A~H, 用path数组记录操作。

剪枝优化:

  1. 需要按照字典序, 那就按照A~H的顺序进行操作
  2. 我们每次操作最好情况下是将两个相同变成三个相同, 故每次改变一个格子, 题目要求让中间8个数相同, 可以设定当前最多相同的数字 num, 其数量为s, 损失函数为 8 - s, 即剩下多少步让该数字变成8个。

操作比较繁琐, 我们可以将#编号如下:

       0     1
       2     3
4   5  6  7  8  9 10
      11    12
13 14 15 16 17 18 19
      20    21
      22    23

然后声明一个8*6的op数组, 记录每个操作对应的下标。还有一个 center 数组, 记录中间8个值的下标, 然后再实现operate操作函数即可。

const int N = 8, M = 25;
int op[N][N] = {
    {0,2,6,11,15,20,22},
    {1,3,8,12,17,21,23},
    {10,9,8,7,6,5,4},
    {19,18,17,16,15,14,13},
    {23,21,17,12,8,3,1},
    {22,20,15,11,6,2,0},
    {13,14,15,16,17,18,19},
    {4,5,6,7,8,9,10}
};
int  opposite[8] = {5, 4, 7, 6, 1, 0, 3, 2};
int center[8] = {6,7,8,11,12,15,16,17};
int q[M];
int path[100];
 
 
void operate(int x)
{
    int t = q[op[x][0]];
    for(int i = 0; i < 6; i++) q[op[x][i]] = q[op[x][i + 1]];
    q[op[x][6]] = t;
}
 
int f()
{
    static int sum[8];
    memset(sum, 0, sizeof sum);
    int s = 0;
    for(int i = 0; i < 8; i++)
        sum[q[center[i]]]++;
    for(int i = 1; i < 4; i++) s = max(s, sum[i]);
    return 8 - s;
}
 
 
 
bool dfs(int u, int depth, int last)
{
    if(u + f() > depth) return false;
    if(f() == 0) return true;
    
    for(int i = 0; i < 8; i++)
    {
        if(opposite[i] == last) continue;
        operate(i);
        path[u] = i;
        if(dfs(u + 1, depth, i)) return true;
        operate(opposite[i]);
    }
    return false;
}
 
int main()
{
    while(cin >> q[0], q[0])
    {
        for(int i = 1; i < 24; i++) cin >> q[i];
        int depth = 0;
        while(!dfs(0,depth, -1)) depth++;
        if(!depth) cout << "No moves needed";
        else
        {
            for(int i = 0; i < depth; i++) cout << char(path[i] + 'A');
        }
        cout << endl << q[center[0]] << endl;
    }
    return 0;
}

给定 本书,编号为 。

在初始状态下,书是任意排列的。

在每一次操作中,可以抽取其中连续的一段,再把这段插入到其他某个位置。

我们的目标状态是把书按照 的顺序依次排列。

求最少需要多少次操作。如果最少操作次数大于或等于 次,则输出 5 or more。

题意是给一串乱序序列, 每次操作可以把一段连续的部分摘出来, 插入到任意位置。求最少用多少步操作能变成正序。

image-20230503165840168

这里比如把 i-j 部分放到k位置上, 只需要把绿色部分和蓝色部分换一下位置即可。

考虑爆搜。 搜索顺序: u为当前变换次数, depth为最大搜索深度

进行到某一步时, 枚举所有摘出来的区间, 然后再枚举放的位置。 返回条件就是遍历一遍当前序列, 若为正序则递归返回。

剪枝优化:

  1. 采用迭代加深的方式搜索, 因为题目要求不需要搜的超过4步
  2. 插入时只插入到k后面, 组合式搜索避免重复
  3. 因为最终状态是全为正序, 对于每次操作的贡献可以通过统计操作后的相邻逆序对数量, 如果减少说明是有效操作, 若增加或者不变则为无效操作。故可以使用IDA-star的优化方式, 定义损失函数计算达成结果的最小步数, 若当前步数加上最小步数大于最大深度, 则直接回溯。
const int N = 15;
int q[N], w[5][N];
int n;
 
int f()
{
    int tot = 0;
    for(int i = 0; i + 1 < n; i++)
        if(q[i + 1] != q[i] + 1) tot++;
        
    return (tot + 2) / 3;
}
 
 
bool dfs(int u, int depth)
{
    if(u + f() > depth) return false;
    if(f() == 0) return true;
    
    for(int len = 1; len < n ; len++)
    {
        for(int i = 0; i + len - 1 < n; i++)
        {
            int j = i + len - 1;
            for(int k = j+1; k < n; k++)
            {
                // 将i-j放到k位置后面
                memcpy(w[u], q, sizeof q);
                int y = i;
                for(int x = j+1; x <= k; x++, y++) q[y] = w[u][x];
                for(int x = i; x <= j; x++, y++) q[y] = w[u][x];
                if(dfs(u+1,depth))
                {
                    /*cout << u << ": ";
                    for(int i = 0; i < n; i++) cout << w[u][i] << " ";
                    cout << endl;*/
                    return true;
                }
                memcpy(q, w[u], sizeof q);
            }
        }
    }
    return false;
}
 
 
int main()
{
    int T;
    cin >> T;
    while(T--)
    {
        cin >> n;
        for(int i = 0; i < n; i++)   cin >> q[i];
        
        int depth = 0;
        while(depth < 5 && !dfs(0,depth)) depth++;
        if(depth >= 5) cout << "5 or more\n";
        else cout << depth << endl;
    }
    return 0;
}

2.2.2 二进制枚举

image-20230503165945252

用二进制枚举所有点走过的方案, 然后用DFS判断当前状态是否成立。

我们设一个长度为 17 的二进制串来记录 1~17编号的格子 是否走过。对于每一个状态 , 先判断起点是否被选中, 即 是否为1。如果选中才会从该点开始搜索。

对于不规则形状的二维坐标, 且每个点都有唯一的编号, 我们可以用一个二维数组g映射 , 同时用 来实现 的映射。

DFS中, 定义一个二进制状态 来更新当前已经走过的位置, 如果当前 , 并且用的筛子数小于筛子总数, 那说明当前的状态成立。

枚举时判断 当前位置的id值 所对应的点是否被走过, 以及是否在目标状态中, 否则就返回不走。

为了方便处理, 我们把每个点的价格转化为需要用的筛子数 。

const int N = 18, M = 5;
int x[] = {1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 5};
int y[] = {1, 2, 4, 5, 2, 3, 4, 2, 3, 4, 2, 3, 4, 1, 2, 4, 5};
int g[N][N];
int w[N];
int n;
int dx[] = {1, -1, 0, 0}, dy[] = {0, 0, 1, -1};
int end_state, cnt, ans;
 
bool dfs(int a, int b, int state)
{
    if (end_state == state && cnt <= n)
        return true; // 可以满足
 
    int id = g[a][b]; // 找对应的价格
    if (id == -1 || !(state >> id & 1) || (end_state >> id & 1))
        return false;
 
    end_state |= 1 << id;
    cnt += w[id];
 
    for (int i = 0; i < 4; i++)
        if (dfs(a + dx[i], b + dy[i], state))
            return true;
    return false;
}
 
int main()
{
    memset(g, -1, sizeof g);
    for (int i = 0; i < 17; i++)
        g[x[i]][y[i]] = i;
 
    int T;
    cin >> T;
    while (T--)
    {
        ans = 0;
        for (int i = 0; i < 17; i++)
            cin >> w[i], w[i] = (w[i] + 5) / 6;
        cin >> n;
        for (int i = 0; i < 1 << 17; i++)
        {
 
            if (!(i >> 13 & 1))
                continue;
 
            end_state = 0, cnt = 0;
            if (dfs(x[13], y[13], i))
                ans = max(ans, (int)__builtin_popcount(i));
        }
        cout << ans << endl;
    }
    return 0;
}

2.2.3 分组问题

翰翰和达达饲养了 只小猫,这天,小猫们要去爬山。

经历了千辛万苦,小猫们终于爬上了山顶,但是疲倦的它们再也不想徒步走下山了(呜咕>_<)。

翰翰和达达只好花钱让它们坐索道下山。

索道上的缆车最大承重量为 ,而 只小猫的重量分别是 。

当然,每辆缆车上的小猫的重量之和不能超过 。

每租用一辆缆车,翰翰和达达就要付 美元,所以他们想知道,最少需要付多少美元才能把这 只小猫都运送下山?

搜索顺序类似于分成互质组, 枚举组数。

这里剪枝是终点: 首先看能不能优化搜索顺序, 先放轻的猫还是先放重的猫。显然是重的猫分支少。 故这里把猫排序, 从重的开始枚举。

const int N = 20;
int n, w;
int a[N];
int sum[N];
int ans = 20;
void dfs(int u, int k)
{
    if (k >= ans)
        return;
    if (u == n)
    {
        ans = k;
        return;
    }
    for (int i = 0; i < k; i++)
    {
        if (sum[i] + a[u] <= w)
        {
            sum[i] += a[u];
            dfs(u + 1, k);
            sum[i] -= a[u];
        }
    }
    sum[k] = a[u];
    dfs(u + 1, k + 1);
    sum[k] = 0;
}
 
int main()
{
    cin >> n >> w;
    for (int i = 0; i < n; i++)
        cin >> a[i];
 
    sort(a, a + n, greater<int>());
    dfs(0, 0);
 
    cout << ans << endl;
    return 0;
}

2.2.4 剪枝优化

  1. 优化搜索顺序 大部分情况下, 我们应该优先搜索分支较少的节点。image-20230503170352553
  2. 排除等效冗余 不搜索重复状态, 比如123可以搜12和21, 保证搜过12就不搜21, 即组合式搜索。
  3. 可行性/最优化剪枝 比如全局更新ans, 若当前再往下搜已经大于之前的最优解, 那么就不必再继续搜了。
  4. 记忆化搜索

你需要把一个 的数独补充完整,使得数独中每行、每列、每个 的九宫格内数字 均恰好出现一次。

image-20230503170414103

求一种填数方案, 使得结果满足:

  • 每行1~9只出现一次
  • 每列1~9只出现一次
  • 每个格子1~9只出现一次 当枚举其中任意一个空位时, 我们需要知道当前能填哪些数, 即需要记录此时行状态, 列状态, 格子状态。他们都是有9种选择, 故可以用9位二进制数, 表示当前行, 列, 格子内可以选那那些数。 比如 col[i], i=000001010, 则可以选2, 4两个数。 这样求三个条件的并集也很方便, 二进制与就可以。 可以先求出所有二进制对应的1的个数ones[N], 和 将lowbit运算返回的1转换为个位数对应的map[N]。

题目给的是一行输入, 也就是二维数组的一维形式, 即g[i][j]的一维形式为g[i*m+j] m为行长度。 搜索顺序为: 先随意选择一个空格子, 再枚举填某个数得到一个新的棋盘, 然后再进行之前的操作直到填满。

优化方法: 在选空格子时, 应该选择分支较少的, 也就是能选的数比较少的 怎么快速求出当前分支较少的空格子呢?没发, 直接暴力搜吧。

const int N = 82, M = 9;
char g[N];
int row[M], col[M], cell[M][M];
int ones[1 << M], map[1 << M];
int cnt;
 
void draw(int x, int y, int t, bool isset)
{
    if (isset)
        g[x * 9 + y] = char(t + '1');
    else
        g[x * 9 + y] = '.';
 
    int v = 1 << t;
    if (isset)
        v = -v;
    row[x] += v;
    col[y] += v;
    cell[x / 3][y / 3] += v;
}
 
int lowbit(int x)
{
    return x & -x;
}
 
int get(int x, int y) // x,y位置上能填那些数
{
    return row[x] & col[y] & cell[x / 3][y / 3];
}
 
bool dfs(int cnt)
{
    if (!cnt)
        return true;
    int x, y, minv = 10;
    for (int i = 0; i < 9; i++)
        for (int j = 0; j < 9; j++)
            if (g[i * 9 + j] == '.')
            {
                int state = get(i, j);
                if (ones[state] < minv)
                {
                    minv = ones[state];
                    x = i, y = j;
                }
            }
    int state = get(x, y);
    for (int i = state; i; i -= lowbit(i))
    {
        int v = map[lowbit(i)]; // 数
        draw(x, y, v, true);
        if (dfs(cnt - 1))
            return true;
        draw(x, y, v, false);
    }
    return false;
}
 
int main()
{
    for (int i = 0; i < 9; i++)
        map[1 << i] = i;
    for (int i = 0; i < 1 << 9; i++)
        for (int j = 0; j < 9; j++)
            ones[i] += i >> j & 1;
    while (cin >> g)
    {
        if (g[0] == 'e')
            break;
        // 初始化什么都能选
        for (int i = 0; i < 9; i++)
            row[i] = col[i] = (1 << M) - 1;
        for (int i = 0; i < 3; i++)
            for (int j = 0; j < 3; j++)
                cell[i][j] = (1 << M) - 1;
        cnt = 0;
        for (int i = 0, k = 0; i < 9; i++)
            for (int j = 0; j < 9; j++, k++)
            {
                char c = g[k];
                if (c != '.')
                    draw(i, j, c - '1', true);
                else
                    cnt++;
            }
 
        dfs(cnt);
        cout << g << endl;
    }
    return 0;
}

2.2.5 双向DFS

达达帮翰翰给女生送礼物,翰翰一共准备了 个礼物,其中第 个礼物的重量是 。

达达的力气很大,他一次可以搬动重量之和不超过 的任意多个物品。

达达希望一次搬掉尽量重的一些物品,请你告诉达达在他的力气范围内一次性能搬动的最大重量是多少。

,

就是从中选择一些数相加, 他们的和需要小于, 求能构成的最大数。如果用动态规划背包做的话, 时间复杂度为 , 这里体积为 , 肯定超时。

考虑爆搜, 搜索顺序: u为当前枚举的位置, s为当前得到的和

剪枝优化:

  1. 先搜分支小的, 故为逆向搜索
  2. 采用双向DFS搜索, 节省时间

双向DFS搜索是指从起点和终点同时搜索, 交汇时就是最终结果, 相比从起点搜到非常深的位置, 这样会少搜一些。用空间换时间。

该题的话就设定一个weight存前半个数组的所有相加和结果, 先DFS前一半数, 每个数选和不选两种情况, 先搜不选的情况, 然后判断是否会超过w(剪枝)再搜选的情况。 其时间复杂度为 即 , n-k为前半部分搜索的复杂度, k为二分搜索 中最大的 , 也就是最大的 。 这里则设定 , 复杂度为 。 若按之前设定为 , 复杂度为

DEBUG 段错误时可以用 exit(0) 来在某行退出, 若可以则在其之上的代码都没问题

const int N = 47;
typedef long long LL;
int a[N];
int n, w, k;
int weight[1 << 25], cnt = 1;
int ans;
void dfs1(int u, int s)
{
    if (u == k)
    {
        weight[cnt++] = s;
        return;
    }
 
    dfs1(u + 1, s);
    if (a[u] <= w - s)
        dfs1(u + 1, s + a[u]);
}
 
void dfs2(int u, int s)
{
    if (u >= n)
    {
        int l = 0, r = cnt - 1;
        while (l < r)
        {
            int mid = l + r + 1 >> 1;
            if (weight[mid] <= w - s)
                l = mid;
            else
                r = mid - 1;
        }
        // cout << s << " " << weight[l] << endl;
        ans = max(ans, weight[l] + s);
        return;
    }
 
    dfs2(u + 1, s);
    if (a[u] <= w - s)
        dfs2(u + 1, a[u] + s);
}
 
int main()
{
    cin >> w >> n;
 
    for (int i = 0; i < n; i++)
        cin >> a[i];
    sort(a, a + n, greater<int>());
    k = n / 2;
    dfs1(0, 0);
 
    sort(weight, weight + cnt);
    cnt = unique(weight, weight + cnt) - weight;
 
    dfs2(k, 0);
 
    cout << ans << endl;
    return 0;
}

2.2.6 迭代加深

满足如下条件的序列 (序列中元素被标号为 )被称为“加成序列”:

  1. 对于每个 ()都存在两个整数 i 和 j (, 和 可相等),使得 。

你的任务是:给定一个整数 ,找出符合上述条件的长度 最小的“加成序列”。

如果有多个满足要求的答案,只需要找出任意一个可行解。

没思路就考虑爆搜, 搜索顺序: u 为当前是第几项, path数组记录当前的数列 1 2 3 … 枚举当前项的值是什么, 从之前几个数相加得出

优化剪枝:

  1. 值越大的分支越少, 从大到小枚举之前的组合
  2. 1+3 和 3+1 得出的值相同, 故以组合方式枚举
  3. 同样的 2+3 和 1+4 的值也相同, 在搜索时声明st布尔数组判断某数是否被没举过
  4. 简单给一个数128, 显然1 2 4 8 16 32 64 128是最短的序列, 其搜索深度为8, 这给了一个启示, 题目n<100, 也就是说正解的层数不深, 但我们如果枚举所有情况的话会搜的非常深, 故可以使用迭代加深优化。
const int N = 110;
int path[N];
int n;
 
bool dfs(int u, int depth)
{
    if(u > depth) return false; // 搜到最后还没搜到就是失败了
    if(path[u - 1] == n) return true;
 
    
    bool st[N] = {0};
    for(int i = u - 1; i >= 0; i--)
        for(int j = i; j >= 0; j--)
        {
            int t = path[i] + path[j];
            if(t > n || st[t] || t <= path[u - 1]) continue;
            
            st[t] = true;
            path[u] = t;
            if(dfs(u + 1, depth)) return true;
        }
    return false;
}
 
int main()
{
    path[0] = 1;
    while(cin >> n, n)
    {
        int depth = 1;
        while(!dfs(1,depth))
            depth++;
        for(int i = 0; i < depth; i++)
            cout << path[i] << " ";
        cout << endl;
    }
    return 0;
}

3 动态规划

3.1 最长上升子序列

 const int N = 1e5 + 10;
int a[N];
int f[N], g[N];
 
int main()
{
    int t, n = 0;
    int cnt1 = 0, cnt2 = 0;
    while (cin >> t && t)
    {// 最长下降子序列
        int pos1 = upper_bound(f, f + cnt1, t, greater<int>()) - f;
        if (pos1 == cnt1)
            f[cnt1++] = t;
        else
            f[pos1] = t;
// 最长上升子序列
        int pos2 = lower_bound(g, g + cnt2, t) - g;
        if (pos2 == cnt2)
            g[cnt2++] = t;
        else
            g[pos2] = t;
    }
    cout << cnt1 << endl;
    cout << cnt2 << endl;
    return 0;
}

3.1.1 应用1:一个序列如何用上升序列和下降序列覆盖, 且所用序列数最小

直接暴力搜索所有情况。

用dfs来求全局最小值有两种方法:全局变量和迭代加深。 全局变量就是声明一个res变量存最终结果, 每搜到一个结果时就与res比较, 留下最小的那个。

对于每个导弹, 先将其当做用上升子序列覆盖的题来求, 求完后再恢复。然后再用下降子序列覆盖的题来求, 求完后恢复。

const int N = 55;
int q[N], up[N], down[N];
int ans, n;
 
void dfs(int u, int su, int sd)
{
    if (su + sd >= ans)
        return;
    if (u == n)
    {
        ans = su + sd;
        return;
    }
    // 当前数放入上升子序列中
    int k = 0;
    while (k < su && up[k] < q[u])
        k++;
    int t = up[k];
    up[k] = q[u];
    if (k < su)
        dfs(u + 1, su, sd);
    else
        dfs(u + 1, su + 1, sd);
    up[k] = t;
 
    // 当前数放到下降子序列中
    k = 0;
    while (k < sd && down[k] > q[u])
        k++;
    t = down[k];
    down[k] = q[u];
    if (k < sd)
        dfs(u + 1, su, sd);
    else
        dfs(u + 1, su, sd + 1);
    down[k] = t;
}
 
int main()
{
    while (cin >> n, n)
    {
        for (int i = 0; i < n; i++)
            cin >> q[i];
        ans = n;
        dfs(0, 0, 0);
        cout << ans << endl;
    }
    return 0;
}

二分法:

// 最长上升子序列, 同一位置上越小越好
	int l = -1, r = su + 1;
    while(l + 1 != r)
    {
        int mid = l + r >> 1;
        if(up[mid] < a[u]) // 求第一个>=a[u]的, 也就是r
            l = mid;
        else 
            r = mid;
    }
    int t = up[r];
    up[r] = a[u];
    if(r != su + 1) // 初始时全为0则全都是比a[u]小的, 故 r 为默认值不变
        dfs(u + 1, su, sd);
    else
        dfs(u + 1, su + 1, sd);
    up[r] = t;
// 最长下降子序列, 同一位置上越大越好
    l = -1, r = sd + 1;
    while(l + 1 != r)
    {
        int mid =l + r >> 1;
        if(down[mid] > a[u]) // 求第一个 <= a[u]的替换掉, 有可能是0(默认值)
            l = mid;
        else
            r = mid;
    }
    t = down[r];
    down[r] = a[u];
    if(r != sd) // 默认为0,故肯定能找到位置sd为0,如果确实在sd说明前面都能比a[u]大, 故增加长度
        dfs(u + 1, su, sd);
    else
        dfs(u + 1, su, sd + 1);
    down[r] = t;

3.1.2 最长公共上升子序列

const int N = 3e3+ 10;
int a[N], b[N];
int f[N][N];
int n,m;
 
int main()
{
    cin >> n;
    for(int i = 1; i <= n;i ++) cin >> a[i];
    for(int i = 1; i <= n;i ++) cin >> b[i];
    
    /*
    for(int i = 1; i <= n;i ++)
        for(int j =1 ; j <= n; j++)
        {
            f[i][j] = f[i - 1][j];
            if(a[i] == b[j])
            {
                f[i][j] = max(f[i][j], 1);
                for(int k = 1; k < j; k++)
                    if(b[k] < b[j])
                        f[i][j] = max(f[i][j], f[i][k] + 1);
            }
        }
    */
    
    for(int i = 1; i <= n;i ++)
    {
        int maxv =1;
        for(int j =1 ; j <= n; j++)
        {
            f[i][j] = f[i - 1][j];
            if(a[i] == b[j]) f[i][j] = max(f[i][j], maxv);
            else if(a[i] > b[j]) maxv = max(maxv, f[i - 1][j] + 1);
        }
    }
        
    int res = 0; 
    for(int i = 1; i <= n;i ++) res = max(res , f[n][i]);
    cout << res << endl;
 
    
    return 0;
}

3.2 背包问题

除了完全背包问题, 其余所有背包问题优化到1维之后都是从大到小循环

for 物品
	for 体积
		for 决策

完全背包:求所有前缀的最大值 多重背包:求滑动窗口的最大值

3.2.1 01背包

当状态不合法时, 取正负无穷。

体积最多为j时, 全部为0, 不能有 j- v < 0 体积恰好为j时, f[0][0] = 0 其余无限大. 不能有 j-v<0 体积至少为j时, f[0][0] = 0 其余无限大, 可以有 j-v < 0, 不过需要将 f[j-v] 变成 f[0], 不能省略, 需要重复计算这一步。

const int N = 1e3 + 10;
int w[N], v[N];
int f[N];
 
int main()
{
    int n, m;
    cin >> n >> m;
    for (int i = 1; i <= m; i++)
        cin >> w[i] >> v[i];
    for (int i = 1; i <= m; i++)
        for (int j = n; j >= w[i]; j--)
            f[j] = max(f[j], f[j - w[i]] + v[i]);
 
    cout << f[n] << endl;
    return 0;
}

3.2.1.1 二维版本

两个价值 w1(氧气), w2(氮气), 一个重量 v, 求 w1, w2 至少为 n,m 时的最小总重量 v。

f[k][i][j]: k是选择前k个物品, 氧气量为i, 氮气量为j 的总气缸重量

初始化的话因为求的是最小值, 故把所有值都初始化为极大值。 f[0][0] = 0

然后正常01背包即可, 注意从 N, M(最大氧气和氮气)开始逆序枚举, 因为求的是至少, 可以超过。

最后再枚举 n-N, m-M, 求最小的重量即可。

const int N = 50, M = 100;
int f[N][M];
int n, m, t;
 
int main()
{
    cin >> n >> m >> t;
    memset(f, 0x3f, sizeof f);
    f[0][0] = 0;
    for (int i = 0; i < t; i++)
    {
        int v1, v2, w;
        cin >> v1 >> v2 >> w;
        for (int j = N - 1; j >= v1; j--)
            for (int k = M - 1; k >= v2; k--)
                f[j][k] = min(f[j][k], f[j - v1][k - v2] + w);
    }
 
    int res = 0x3f3f3f3f;
    for (int i = n; i <= N - 1; i++)
        for (int j = m; j <= M - 1; j++)
            res = min(res, f[i][j]);
 
    cout << res << endl;
 
    return 0;
}

3.2.1.2 例题:吃能量石

收集了 N 块能量石准备开吃。吃完第 i 块能量石需要花费的时间为 Si 秒。

第 i 块能量石最初包含 Ei 单位的能量,并且每秒将失去 Li 单位的能量。

当杜达开始吃一块能量石时,他就会立即获得该能量石所含的全部能量(无论实际吃完该石头需要多少时间)。

能量石中包含的能量最多降低至 0。

请问杜达通过吃能量石可以获得的最大能量是多少?

对于一个复杂问题, 难以求解时就寻找有没有性质可以缩小最优解范围。

对于每个石头, 我们没必要同时处理所有, 显然当一个石头能量值为0时便不用花时间去吃。 性质1: 只吃能量还存在的石头 这个性质显而易见, 接下来就选两个石头看怎么选择 对于这两个石头都吃时所获得的能量有两个情况:

  1. 先吃i后吃i+1 e[i] + e[i + 1] - s[i] * l[i + 1]
  2. 先吃i+1后吃i e[i+1] + e[i] - s[i+1] * l[i] 显然, 这里有个最优选择, 选择 s 最小的那个先吃。

也就是说, 对于所有可行吃法, 最优解肯定符合, 吃的顺序是s[i] * l[i+1]从小到大的。 性质2: 吃石头的最优解中满足任意两个石头, 先吃的 s[i] * l[i + 1] < s[i + 1] * l[i]

那么我们就将所有石头排个序, 对于石头i, 不可能再从i之前的石头中得到比当前更优的解法, 故 f[i - 1] 是固定的, 不会因后续的新石头而变化, 因此, 就跟01背包问题相类似了。

带入01背包的框架思考一下: 对于石头i有选和不选两种选择 选: f[i][j] = f[i - 1][j - s[i]] + e[i] - (j - s[i])*l[i]; 不选: f[i][j] = f[i - 1][j];

初始化时应该初始化为负无穷, 时间恰好是j: 以前能算到至多是 j 是因为dp[0][j]都是0,即从i=0时从哪个体积开始都是合法的, 相当于把空余的体积加在了最前面,但现在这样不是最优,把空余时间加在最后面和最前面不一样。 多的时间会导致能量随时间减少。

const int N = 110, T = 1e7 + 10;
int f[T];
int n;
 
struct Stone
{
    int s,e,l;
    bool operator < (const Stone &W)const
    {
        return s * W.l < W.s * l;
    }
}stone[N];
 
int main()
{
    int k, kase=0;
    cin >> k;
    while(k--)
    {
        int m = 0;
        cin >> n;
        for(int i = 0; i < n; i++)
        {
            int s,e,l;
            cin >> s >> e >> l;
            stone[i] = {s,e,l};
            m += s; // 最大用时就是把所有石头都吃掉
        }
        sort(stone, stone + n);
        memset(f, 0, sizeof f);
        for(int i = 0; i < n; i++)
        {
            int s = stone[i].s, e = stone[i].e, l = stone[i].l;
            for(int j = m; j >= s; j--)
            {
                f[j] = max(f[j], f[j - s] + e - (j - s) * l);
                //cout << s << " " << e << " " << l << " " << f[j] <<  endl;
            }     
        }  
        int res = 0;
        for(int i = 0; i <= m; i++)
            res = max(res, f[i]);
        printf("Case #%d: %d\n", ++kase, res);
        
    }
    return 0;
}

3.2.2 有依赖的背包

3.2.2.1 每组只需要选一个

const int N = 1010, M = 1010;
int v[N][M];
int f[N][M];
 
int n, m;
 
int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            cin >> v[i][j];
    memset(f, 0, sizeof f);
    // 依赖背包DP
    for (int i = n; i >= 1; i--)
    {
        for (int j = 0; j <= m; j++)
        {
            for (int k = 0; k <= j; k++)
                f[i][j] = max(f[i][j], f[i + 1][j - k] + v[i][k]);
        }
    }
    // 背包求方案
    cout << f[1][m] << endl;
    for (int i = 1, j = m; i <= n; i++)
    {
        int res = 0;
        for (int k = 0; k <= j; k++)
            if (f[i][j] == f[i + 1][j - k] + v[i][k])
            {
                res = k;
                j -= k;
                break;
            }
        cout << i << " " << res << endl;
    }
    return 0;
}

3.2.2.1 分组数量较小, 用二进制枚举

const int N = 4e4 + 10, M = 100;
// 前i个物品, 钱数为j, 最大的钱数重要度乘积和
int f[N];
typedef pair<int, int> PII;
PII master[M];
vector<PII> servent[M];
 
int main()
{
    int n, m;
    cin >> n >> m;
 
    for (int i = 1; i <= m; i++)
    {
        int v, w, q;
        cin >> v >> w >> q;
        if (!q)
            master[i] = {v, v * w};
        else
            servent[q].push_back({v, v * w});
    }
 
    for (int i = 1; i <= m; i++)
    {
        if (master[i].first)
        {
            for (int j = n; j >= 0; j--)
            {
                auto &sv = servent[i];
                for (int k = 0; k < 1 << sv.size(); k++)
                {
                    int v = master[i].first, w = master[i].second;
                    for (int u = 0; u < sv.size(); u++)
                    {
                        if (k >> u & 1)
                            v += sv[u].first, w += sv[u].second;
                    }
                    if (j >= v)
                        f[j] = max(f[j], f[j - v] + w);
                }
            }
        }
    }
    cout << f[n] << endl;
    return 0;
}

3.2.2.2 分组数量较大, 采用体积划分法, 树形DP

即对于每个子树, 先确定能用的最大体积, 然后再依次为基础枚举所有能用的体积来转移。

先比较一下以往 线性背包DP 的 状态转移,第 i 件 物品 只会依赖第 i−1 件 物品 的状态

如果本题我们也采用该种 状态依赖关系 的话,对于节点 i,我们需要枚举他所有子节点的组合 2k 种可能

再枚举 体积,最坏时间复杂度 可能会达到 (所有子节点都依赖根节点) 最终毫无疑问会 TLE

因此我们需要换一种思考方式,那就是枚举每个 状态 分给各个子节点 的 体积

这样 时间复杂度 就是

状态表示: f[i][j] 以i为根, 背包容量为 j 的最大价值

const int N = 110, M = N * 2;
int h[N], e[M], v[M], w[M],ne[M], idx;
int n,m;
int f[N][N]; // 以i为根的树, 背包容量为j 时的最大价值
int root;
 
void add(int a, int b)
{
    e[idx] = b,ne[idx] = h[a], h[a] = idx++;
}
 
void dp(int u)
{
    for(int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        dp(j);
        
        for(int k = m - v[u]; k >= 0; k--)
            for(int q = 0; q <= k; q++)
                f[u][k] = max(f[u][k], f[u][k - q] + f[j][q]);
    }
    for(int i = m; i >= v[u]; i--)
        f[u][i] = f[u][i - v[u]] + w[u];
    for(int i = 0; i < v[u]; i++)
        f[u][i] = 0;
}
 
int main()
{
    memset(h, -1, sizeof h);
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        int p;
        cin >> v[i] >> w[i] >> p;
        if(p == -1) root = i;
        else {
            add(p, i);
        }
    }
    dp(root);
    cout << f[root][m] << endl;
    return 0;
}

3.2.3 完全背包

3.2.3.1 朴素

int main()
{
    cin >>n ;
    for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
    for(int i = 1; i < =n ;i ++)
        for(int j = 0; j <= m; j++)
            for(int k = 0; k * v[i] <= j; k++)
                f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
    cout << f[n][m] << endl;
}

3.2.3.2 优化k

int main()
{
    cin >> n;
    for(int i = 1; i <=n ;i++) cin >> v[i] >> w[i];
    for(int i = 1; i <= n; i++)
        for(int j = 0; j <= m; j++)
        {
            f[i][j] = f[i - 1][j];
            if(v[i] <= j)
                f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
        }
    cout << f[n][m] << endl;
}

3.2.3.3 化成一维

int main()
{
    cin >> n;
    for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
    for(int i = 1; i <=n ;i++)
        for(int j = v[i]; j <= m; j++)
            f[j] = max(f[j], f[j - v[i]] + w[i]);
    cout << f[m] << endl;
    return 0;
}

3.2.4 多重背包

3.2.4.1 数据范围小

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++) cin >> v[i] >> w[i] >> s[i];
 
    for(int i = 1; i <= n; i++)
        for(int j = 0; j <= m; j++)
            for(int k = 0; k <= s[i] && k * v[i] <= j; k++) 
                f[i][j] = max(f[i][j], f[i - 1][j - v[i] * k] + w[i] * k);
    cout << f[n][m] << endl;
    return 0;
}

3.2.4.2 体积和物品数小于2000时

用 2^n(n = 0,1,2,3,…,k)(2^k+1 > s) 分组,为什么说少了很多组却仍能得到正确结果呢? 因为当你枚举到 选 3 个物品时,前面已经枚举过的 选1 和 选2 可以组成 选3, 也就是说 选1 和 选2 的结果跟你 选3 是等价的, 那么我们就可以把 选3 优化掉不去枚举。

怎么跟 01 背包问题扯上关系呢? 你划分之后的物品可以抽象成 “新” 的物品 [打包的物品] (s) (s s) (s s s s ) ( s s s s…) … [对应] 1 2 4 8 … 那么对每个新物品做01背包,便能求出正解。 对于 选3 (s s s),可以由选择过 选1 和 选2 的状态表示,同样 选5 也能由 更小的状态表示出来

const int N = 25000, M = 2010; // N = 1000 * log 2000 (物品种类数 * log 每种物品的个数)
int n,m, cnt; // cnt 将每种物品二进制展开后的数组下标
int v[N],w[N];
int f[M]; // 01背包的一维优化
int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        int a, b, c;
        cin >> a >> b >> c;
        int k = 1;
        while(k  <= c)
        {
            v[++cnt] = a * k;
            w[++cnt] = b * k;
            c -= k;
            k *= 2;
        }
        if(c > 0) // 最后的那个 c
        {
            v[++cnt] = a * c;
            w[++cnt] = b * c;
        }
    }
    int n = cnt;
    for(int i = 1; i <= cnt; i++)
        for(int j = m; j >= v[i]; i--)
            f[j] = max(f[j], f[j - v[i]] + w[i]);
    cout << f[m];
    return 0;
}

3.2.4.3 体积和物品数小于20000时

状态表示依然是前i个物品, 背包体积为j的最大价值。 划分为当前i物品选 0, 1, 2, 3,,, s个

f[i][j] = 
            f[i-1][j],     f[i-1][j-v]+w,   f[i-1][j-2v]+2w,,,f[i-1][j-sv]+sw}
f[i][j-v]= 
            f[i-1][j-v],   f[i-1][j-2v]+w,  f[i-1][j-3v]+2w,,,f[i-1][j-sv]+(s-1)w, f[i-1][j-(s+1)v]+sw}
f[i][j-2v]=
            f[i-1][j-2v],  f[i-1][j-3v]+w,  f[i-1][j-4v]+2w,,,f[i-1][j-sv]+(s-2)w, f[i-1][j-(s+1)v]+(s-1)w, f[i-1][j-(s+2)v]+sw}
const int N = 2e4 + 10;
int n,m;
int q[N], g[N], f[N];
const int V[] = {1,2,3,4};
const int W[] = {2,4,4,5};
const int S[] = {3,1,3,2};
int main()
{
    //cin >> n >> m;
    for(int i = 0; i < n; i++)
    {
        int v = V[i],w = W[i],s = S[i];
        cin >> v >> w >> s;
        memcpy(g, f, sizeof f);
        for(int r = 0; r < v; r++)
        {
            int hh =0, tt = -1;
            for(int j = r; j <= m; j += v)
            {
                if(hh <= tt && q[hh] < j - s*v) hh++;
                if(hh <= tt) f[j] = max(f[j], g[q[hh]] + (j - q[hh]) / v * w);
                while(hh <= tt && g[q[tt]] - (q[tt] - r)/v*w <= g[j] - (j - r)/v * w)tt--;
                q[++tt] = j;
            }
        }
    }
    cout << f[m] << endl;
    return 0;
}

3.2.5 混合背包

有 种物品和一个容量是 的背包。

物品一共有三类:

  • 第一类物品只能用1次(01背包);
  • 第二类物品可以用无限次(完全背包);
  • 第三类物品最多只能用 sisi 次(多重背包);
const int N = 1e4 + 10, M = 1e4 + 10;
int f[M];
int n,m;
 
int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        int v,w,s;
        cin >> v >> w >>s;
        if(s == 0) // 完全
        {
            for(int j = v; j <= m; j ++)
                f[j] = max(f[j], f[j - v] + w);
            
        }
        else 
        {
            if(s == -1) s = 1;
            for(int k = 1; k <= s; k *= 2)
            {
                for(int j = m; j >= k * v; j--)
                f[j] = max(f[j], f[j - k * v] + k * w);
                s -= k;
            }
            if(s)
            {for(int j = m; j >= s * v; j--)
                f[j] = max(f[j], f[j - s * v] + s * w);
            }
        }
        
    }
    cout << f[m] << endl;
    return 0;
}

3.2.6 背包问题求方案数

用 g 数组记录当前 状态 的最大方案数 显然在f[i][j] 转移时有两个方向: f[i - 1][j] 和 f[i - 1][j - v[i]] + w[i] 若这两个不相等说明选择了其中一个方案, 若相同则将两个的方案数都加一起。

const int N = 1e3 + 10, mod = 1e9 + 7;
int f[N];
int g[N];
int w[N],v[N];
int n,m;
 
 
int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
        cin >> v[i] >> w[i];
    
    g[0] = 1;
    
    for(int i = 1; i <= n; i++)
    {
        for(int j = m; j >= v[i]; j--)
        {
            
            int maxv = max(f[j - v[i]] + w[i], f[j]);
            int cnt = 0;
            if(maxv == f[j - v[i]] + w[i]) cnt = g[j - v[i]];
            if(maxv == f[j]) cnt = (cnt + g[j]) % mod;
            f[j] = maxv;
            g[j] = cnt;
        }
    }
    int cnt = g[m];
    for(int i = 0; i < m; i++)
        if(f[m] == f[i])
            cnt = (cnt + g[i]) % mod;
        
    
    //cout << f[n][m] << endl;
    cout << cnt << endl;
    return 0;
    
}

3.2.7 背包问题求具体方案

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。

第 i 件物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

输出 字典序最小的方案。

题目要求按字典序排列, 那我们就不能从 n-1 方向来求最短路, 需要用 1-n 来求, 根据反方向原则, 在求DP时要用n-1方向。

const int N = 1010;
 
int f[N][N];
int n,m;
int v[N],w[N];
 
int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++) cin >> v[i] >> w[i];
    
    for(int i = n; i >= 1; i --)
    {
        for(int j = 0; j <= m; j++)
        {
            f[i][j] = f[i + 1][j];
            if(j >= v[i]) f[i][j] = max(f[i][j], f[i + 1][j - v[i]] + w[i]);
        }
    }
    
    int j = m;
    for(int i = 1; i <= n; i++)
    {
        if(j >= v[i] && f[i][j] == f[i + 1][j - v[i]] + w[i])
        {
            cout << i << " ";
            j -= v[i];
        }
    }
    return 0;
}

3.3 区间DP

3.3.1 环形石子合并

在一个圆形操场的四周摆放 堆石子,现要将石子有次序地合并成一堆,规定每次只能选相邻的 堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。

试设计出一个算法,计算出将 堆石子合并成 堆的最小得分和最大得分。

const int N = 420, INF = 0x3f3f3f3f;
int n;
int w[N], s[N];
int f[N][N];
int g[N][N];
 
int main()
{
    while (cin >> n)
    {
 
        for (int i = 1; i <= n; i++)
        {
            cin >> w[i];
            w[i + n] = w[i];
        }
        for (int i = 1; i <= n * 2; i++)
            s[i] = s[i - 1] + w[i];
        mem(f, 0x3f);
        mem(g, -0x3f);
 
        for (int len = 1; len <= n; len++)
        {
            for (int l = 1; l + len - 1 <= n * 2; l++)
            {
                int r = l + len - 1;
                if (len == 1)
                    f[l][r] = g[l][r] = 0;
                else
                {
                    for (int k = l; k < r; k++)
                    {
                        f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
                        g[l][r] = max(g[l][r], g[l][k] + g[k + 1][r] + s[r] - s[l - 1]);
                    }
                }
            }
        }
 
        int maxv = -INF, minv = INF;
        for (int i = 1; i <= n; i++)
        {
            minv = min(minv, f[i][i + n - 1]);
            maxv = max(maxv, g[i][i + n - 1]);
        }
        cout << minv << endl
             << maxv << endl;
    }
    return 0;
}

3.3.2 树形区间DP, 加分二叉树

每个节点都有一个分数(均为正整数),记第 i 个节点的分数为 di,tree 及它的每个子树都有一个加分,任一棵子树 subtree(也包含 tree 本身)的加分计算方法如下:     

subtree的左子树的加分 × subtree的右子树的加分 + subtree的根的分数 

若某个子树为空,规定其加分为 1。

叶子的加分就是叶节点本身的分数,不考虑它的空子树。

试求一棵符合中序遍历为(1,2,3,…,n)且加分最高的二叉树 tree。

要求输出: 

(1)tree的最高加分 

(2)tree的前序遍历

树形区间DP, 要求一段区间内怎么组建树来得到最大权值, 且求出方案。 显然从直觉上就可以划分, 可以确定其中一个点作为根, 然后左边区间为左子树

const int N = 33;
int w[N];
int f[N][N], g[N][N];
int n;
 
void dfs(int l, int r)
{
    if (l > r)
        return;
    int root = g[l][r];
    cout << root << " ";
    dfs(l, root - 1);
    dfs(root + 1, r);
}
 
int main()
{
    cin >> n;
    for (int i = 1; i <= n; i++)
        cin >> w[i];
 
    for (int len = 1; len <= n; len++)
    {
        for (int l = 1; l + len - 1 <= n; l++)
        {
            int r = l + len - 1;
            if (len == 1)
            {
                f[l][r] = w[l];
                g[l][r] = l;
            }
            else
            {
                for (int k = l; k <= r; k++)
                {
                    int left = k == l ? 1 : f[l][k - 1];
                    int right = k == r ? 1 : f[k + 1][r];
                    int score = left * right + w[k];
                    if (f[l][r] < score)
                    {
                        f[l][r] = score;
                        g[l][r] = k;
                    }
                }
            }
        }
    }
    cout << f[1][n] << endl;
    dfs(1, n);
 
    return 0;
}

3.3.3 二维区间DP

将一个 的棋盘进行如下分割:将原棋盘割下一块矩形棋盘并使剩下部分也是矩形,再将剩下的部分继续如此分割,这样割了 次后,连同最后剩下的矩形棋盘共有 块矩形棋盘。(每次切割都只能沿着棋盘格子的边进行)

image-20230503175914599

现在需要把棋盘按上述规则分割成 n 块矩形棋盘,并使各矩形棋盘总分的均方差最小。

均方差formula.png ,其中平均值lala.png , 为第 块矩形棋盘的总分。

状态表示: f[x1][y1][x2][y2][k] 子矩阵(x1,y1) (x2,y2)划分k个部分的最小方差均值。 状态计算: 一个矩阵划分可以竖着划分, 共 (y2 - y1 - 1) * 2种切法, 乘2是因为可以选择保留左边还是右边。 或者横着划分, 共 (x2 - x1 - 1) * 2种切法。 令v = f[x1][y1][x2][y2][k] 竖切:

  • 取上v = min(f[x1][y1][i][y2][k - 1] + get(i, y1, x2, y2)) i in [x1, x2)
  • 取下v = min(f[i][y1][x2][y2][k - 1] + get(x1,y1,i,y2)) i in [x1, x2) 横切:
  • 取左v = min(f[x1][y1][x2][i][k - 1] + get(x1, i, x2, y2)) i in [y1, y2)
  • 取右v = min(f[x1][i][x2][y2][k - 1] + get(x1, y1, x2, i)) i in [y1, y2) 这里用递归的写法会更好, 递推太繁琐了。
const int N = 16, M = 10;
const double INF = 0x3f3f3f3f;
double f[M][M][M][M][N];
double X;
int a[M][M];
int n, m;
 
double get(int x1, int y1, int x2, int y2)
{
    double sum = a[x2][y2] - a[x1 - 1][y2] - a[x2][y1 - 1] + a[x1 - 1][y1 - 1] - X;
    return sum * sum / n;
}
 
double dfs(int x1, int y1, int x2, int y2, int k)
{
    double &v = f[x1][y1][x2][y2][k];
    if (v >= 0)
        return v;
    if (k == 1)
        return get(x1, y1, x2, y2);
    v = INF;
    // 竖切
    for (int i = y1; i < y2; i++)
    {
        v = min(v, dfs(x1, y1, x2, i, k - 1) + get(x1, i + 1, x2, y2));
        v = min(v, dfs(x1, i + 1, x2, y2, k - 1) + get(x1, y1, x2, i));
    }
    // 横切
    for (int i = x1; i < x2; i++)
    {
        v = min(v, dfs(x1, y1, i, y2, k - 1) + get(i + 1, y1, x2, y2));
        v = min(v, dfs(i + 1, y1, x2, y2, k - 1) + get(x1, y1, i, y2));
    }
 
    return v;
}
 
int main()
{
    setPrec(3);
    FasterIO;
    cin >> n;
    m = 8;
    for (int i = 1; i <= 8; i++)
        for (int j = 1; j <= 8; j++)
        {
            cin >> a[i][j];
            a[i][j] += a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1];
        }
    X = (double)a[m][m] / n;
    memset(f, -1, sizeof f);
 
    cout << sqrt(dfs(1, 1, 8, 8, n));
    return 0;
}

3.4 单调队列优化DP

3.4.1 最大子序和

输入一个长度为 n 的整数序列,从中找出一段长度不超过 m 的连续子序列,使得子序列中所有数的和最大。

const int N =3e5 + 10;
 
int n,m;
int hh,tt;
int q[N];
int a[N];
 
 
int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i];
        a[i] += a[i - 1];
    }
    
    int res = -2e9;
    for(int i = 1; i <= n; i++)
    {
        while(q[hh] < i - m) hh++;
        res = max(res, a[i] - a[q[hh]]);
        while(hh <= tt && a[i] <= a[q[tt]]) tt--;
        q[++tt] = i;
    }
    
    cout << res << endl;
    return 0;
    
}

3.4.2 长为m的区间中至少选择一个点, 求最小总代价

状态表示: f[i] 1-i中选, 结尾为i且选中的最小价值 状态计算: f[i] = min(f[j] + w[i]) j in [i - m,i-1]

结果状态为 也可以利用单调队列的特性, 最后是枚举到 [n - m, n - 1]这个区间, 若q[hh] < n + 1 - m , 则hh++, 那么 f[q[hh]] 就是答案。

这里用, 是因为需要计算, 把先入队, 避免出现的起始问题

const int N = 2e5 + 10;
int a[N];
int q[N], hh, tt;
int f[N];
int n, m;
 
int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        cin >> a[i];
 
    hh = 0, tt = 0;
    for (int i = 1; i <= n; i++)
    {
        if (q[hh] < i - m)
            hh++;
        f[i] = f[q[hh]] + a[i];
        while (hh <= tt && f[q[tt]] >= f[i])
            tt--;
        q[++tt] = i;
    }
 
    if (n - m + 1 > q[hh])
        hh++;
    cout << f[q[hh]] << endl;
    return 0;
}

3.4.3 最大值最小+区间m至少选一点

高二数学《绿色通道》总共有 n 道题目要抄,编号 ,抄第 题要花 分钟。 小 决定只用不超过 分钟抄这个,因此必然有空着的题。 每道题要么不写,要么抄完,不能写一半。 下标连续的一些空题称为一个空题段,它的长度就是所包含的题目数。 这样应付自然会引起马老师的怙怒,最长的空题段越长,马老师越生气。 现在,小 想知道他在这 分钟内写哪些题,才能够尽量减轻马老师的怒火。 由于小 很聪明,你只要告诉他最长的空题段至少有多长就可以了,不需输出方案。

对于最大值最小问题一般先考虑二分答案。丢瓶盖 有界性: 最长空题段最多时一个都不做, 最小的全做, 即 两段性: 最长空题段为 时, 其最小耗时为 若想空题段长度更小, 需要再做几道题, 即 时, 需满足 若想空题段长度更大, 需要少做几道题, 即 时, 需满足

故证毕, 和丢瓶盖一题很是相似。 接下来就只需要求出空题段不超过的最小耗时即可。 也就是说, 在任意一个长度为 的区间中, 必须存在一个题被做, 这样问题就转化为烽火传递了。

const int N = 5e5 + 10;
int f[N];
int a[N];
int n, m;
int q[N], hh, tt;
 
bool check(int limit)
{
    hh = 0, tt = 0;
    for (int i = 1; i <= n; i++)
    {
        if (q[hh] < i - limit - 1)
            hh++;
        f[i] = f[q[hh]] + a[i];
        while (hh <= tt && f[q[tt]] >= f[i])
            tt--;
        q[++tt] = i;
    }
    for (int i = n - limit; i <= n; i++)
        if (f[i] <= m)
            return true;
    return false;
}
 
int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        cin >> a[i];
 
    int l = -1, r = n + 1;
    while (l != r - 1)
    {
        int mid = l + r >> 1;
        if (check(mid))
            r = mid;
        else
            l = mid;
    }
    cout << r << endl;
    return 0;
}

3.4.4 长度为k的最大正方形

矩阵中所有 正方形区域中的最大整数和最小整数的差值」的最小值。

image-20230503180642152

const int N = 1100;
int a[N][N];
int row_max[N][N], row_min[N][N];
int q[N];
int n, m, k;
 
void get_min(int a[], int b[], int tot)
{
    int hh = 0, tt = -1;
    for (int i = 1; i <= tot; i++)
    {
        if (hh <= tt && i - q[hh] >= k)
            hh++;
        while (hh <= tt && a[q[tt]] >= a[i])
            tt--;
        q[++tt] = i;
        b[i] = a[q[hh]];
    }
}
 
void get_max(int a[], int b[], int tot)
{
    int hh = 0, tt = -1;
    for (int i = 1; i <= tot; i++)
    {
        if (hh <= tt && i - q[hh] >= k)
            hh++;
        while (hh <= tt && a[q[tt]] <= a[i])
            tt--;
        q[++tt] = i;
        b[i] = a[q[hh]];
    }
}
 
int main()
{
    cin >> n >> m >> k;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            cin >> a[i][j];
 
    // 求行
    for (int i = 1; i <= n; i++)
    {
        get_max(a[i], row_max[i], m);
        get_min(a[i], row_min[i], m);
    }
    int res = 1e9;
    int a[N], b[N], c[N];
    // 求列
    for (int i = k; i <= m; i++)
    {
        for (int j = 1; j <= n; j++)
            a[j] = row_min[j][i];
        get_min(a, b, n);
        for (int j = 1; j <= n; j++)
            a[j] = row_max[j][i];
        get_max(a, c, n);
 
        for (int j = k; j <= n; j++)
            res = min(res, c[j] - b[j]);
    }
    cout << res << endl;
    return 0;
}

3.5 数位DP

3.5.1 不考虑前导0

3.5.1.1 一个整数闭区间 ,问这个区间内有多少个不降数。

void init()
{
    for (int i = 0; i <= 9; i++)
        f[1][i] = 1;
    for (int i = 2; i < N; i++)
        for (int j = 0; j <= 9; j++)
            for (int k = j; k <= 9; k++)
                f[i][j] += f[i - 1][k];
}
 
int dp(int num)
{
    if (!num)
        return 1;
 
    vector<int> nums;
    while (num)
        nums.push_back(num % 10), num /= 10;
 
    int res = 0;
    int last = 0;
    for (int i = nums.size() - 1; i >= 0; i--)
    {
        int x = nums[i];
        for (int j = last; j < x; j++)
            res += f[i + 1][j];
        if (x < last)
            break;
        last = x;
        if (!i)
            res++;
    }
 
    return res;
}

3.5.1.2 求给定区间 中满足下列条件的整数个数:这个数恰好等于 个互不相等的 的整数次幂之和。

void init()
{
    for (int i = 0; i < N; i++)
        for (int j = 0; j <= i; j++)
        {
            if (!j)
                f[i][j] = 1;
            else
                f[i][j] = f[i - 1][j - 1] + f[i - 1][j];
        }
}
 
int dp(int u)
{
    if (!u)
        return 0;
 
    vector<int> nums;
    while (u)
        nums.push_back(u % B), u /= B;
 
    int res = 0;
    int last = 0;
    for (int i = nums.size() - 1; i >= 0; i--)
    {
        int x = nums[i];
        if (x) // 计算左分支
        {
            res += f[i][K - last]; // 当前位为0时
            if (x > 1)             // 若当前位可以大于等于1
            {
                if (K - last - 1 >= 0)
                    res += f[i][K - last - 1];
                break;
            }
            else // 说明当前位除了0之外只能为1, 即为最大值
            {
                last++;
                if (last > K)
                    break;
            }
        }
        if (!i && last == K)
            res++;
    }
    return res;
}

3.5.1.3 有多少个数满足各位数字之和 为 。

int mod(int x, int y)
{
    return (x % y + y) % y;
}
 
void init()
{
    memset(f, 0, sizeof f);
    for (int i = 0; i <= 9; i++)
        f[1][i][i % m]++;
 
    for (int i = 2; i < N; i++)
        for (int j = 0; j <= 9; j++)
            for (int k = 0; k < m; k++)
                for (int x = 0; x <= 9; x++)
                    f[i][j][k] += f[i - 1][x][mod(k - j, m)];
}
 
int dp(int n)
{
    if (!n)
        return 1;
 
    vector<int> nums;
    while (n)
        nums.push_back(n % 10), n /= 10;
 
    int res = 0;
    int last = 0; // 存上个数的总和
    for (int i = nums.size() - 1; i >= 0; i--)
    {
        int x = nums[i];
        for (int j = 0; j < x; j++)
            res += f[i + 1][j][mod(-last, m)];
 
        last += x;
 
        if (!i && last % m == 0)
            res++;
    }
 
    return res;
}

3.5.1.4 不吉利的数字为所有含有 或 的号码, 求ab中吉利的数个数

void init()
{
    for (int i = 0; i <= 9; i++)
        if (i != 4)
            f[1][i] = 1;
 
    for (int i = 2; i < N; i++)
        for (int j = 0; j <= 9; j++)
        {
            if (j == 4)
                continue;
            for (int k = 0; k <= 9; k++)
            {
                if (k == 4 || j == 6 && k == 2)
                    continue;
                f[i][j] += f[i - 1][k];
            }
        }
}
 
int dp(int n)
{
    if (!n)
        return 1;
 
    vector<int> nums;
    while (n)
        nums.push_back(n % 10), n /= 10;
 
    int res = 0;
    int last = 0;
    for (int i = nums.size() - 1; i >= 0; i--)
    {
        int x = nums[i];
        for (int j = 0; j < x; j++)
        {
            if (j == 4 || last == 6 && j == 2)
                continue;
            res += f[i + 1][j];
        }
 
        if (x == 4 || (last == 6 && x == 2))
            break;
        else
            last = x;
 
        if (!i)
            res++;
    }
    return res;
}

3.5.1.5 和7无关的平方和

LL mod(LL x, int y)
{
    return (x % y + y) % y;
}
 
void init()
{
    for (int i = 0; i <= 9; i++)
        if (i != 7)
        {
            auto &v = f[1][i][i % 7][i % 7];
            v.s0++;
            v.s1 += i;
            v.s2 += i * i;
        }
 
    LL power = 10;
    for (int i = 2; i < N; i++, power *= 10)
        for (int j = 0; j <= 9; j++)
        {
            if (j == 7)
                continue;
            for (int a = 0; a < 7; a++)
                for (int b = 0; b < 7; b++)
                    for (int k = 0; k <= 9; k++)
                    {
                        if (k == 7)
                            continue;
                        auto &v1 = f[i][j][a][b], &v2 = f[i - 1][k][mod(a - j * (power % 7), 7)][mod(b - j, 7)];
                        v1.s0 = (v1.s0 + v2.s0) % P;
                        v1.s1 = (v1.s1 + j * (power % P) * v2.s0 + v2.s1) % P;
                        v1.s2 = (v1.s2 +
                                 j * j * (power % P) % P * (power % P) % P * v2.s0 % P +
                                 2 * j * (power % P) % P * v2.s1 % P +
                                 v2.s2) %
                                P;
                    }
        }
    power7[0] = power9[0] = 1;
    for (int i = 1; i < N; i++)
    {
        power7[i] = power7[i - 1] * 10 % 7;
        power9[i] = power9[i - 1] * 10ll % P;
    }
}
 
F get(int i, int j, int a, int b)
{
    int s0 = 0, s1 = 0, s2 = 0;
    for (int x = 0; x < 7; x++)
        for (int y = 0; y < 7; y++)
        {
            if (x == a || y == b)
                continue;
            auto &v = f[i][j][x][y];
 
            s0 = (s0 + v.s0) % P;
            s1 = (s1 + v.s1) % P;
            s2 = (s2 + v.s2) % P;
        }
    return {s0, s1, s2};
}
 
LL dp(LL n)
{
    if (!n)
        return 0;
 
    LL backup_n = n % P;
    vector<int> nums;
    while (n)
        nums.push_back(n % 10), n /= 10;
 
    LL res = 0;
    LL last_a = 0, last_b = 0; // a是整个数, b是各个数字和
    for (int i = nums.size() - 1; i >= 0; i--)
    {
        int x = nums[i];
        for (int j = 0; j < x; j++)
        {
            if (j == 7)
                continue;
            // 对于该状态 last_a j ... ... ...
            int a = mod(-last_a % 7 * power7[i + 1], 7); // 即 (j...) + last_a mod 7 != 0, (j...) = -last_a % 7
            int b = mod(-last_b, 7);
 
            auto v = get(i + 1, j, a, b); // 要求是不满足 %7=0, 故其他的a,b取值都可以
            res = (res +
                   ((last_a % P) * (last_a % P) % P * power9[i + 1] % P * power9[i + 1] % P * v.s0 % P +
                    2 * (last_a % P) * power9[i + 1] % P * v.s1 % P +
                    v.s2) %
                       P) %
                  P;
        }
        if (x == 7)
            break;
        last_a = last_a * 10 + x;
        last_b = last_b + x;
 
        if (!i && last_a % 7 != 0 && last_b % 7 != 0)
            res = (res + backup_n * backup_n) % P;
    }
 
    return res;
}

3.5.2 考虑前导0

前导0, 方法是在首尾不加上0开头的部分, 最后再加一遍所有长度小于 num.size() 的部分。

3.5.2.1 a和b之间有几个不含前导零且相邻两个数字之差至少为 2 的正整数

void init()
{
    for (int i = 0; i <= 9; i++)
        f[1][i] = 1;
    for (int i = 2; i < N; i++)
        for (int j = 0; j <= 9; j++)
            for (int k = 0; k <= 9; k++)
                if (abs(j - k) >= 2)
                    f[i][j] += f[i - 1][k];
}
 
int dp(int num)
{
    if (!num)
        return 0;
 
    vector<int> nums;
    while (num)
        nums.push_back(num % 10), num /= 10;
 
    int res = 0;
    int last = -3;
    for (int i = nums.size() - 1; i >= 0; i--)
    {
        int x = nums[i];
        for (int j = i == nums.size() - 1; j < x; j++)
        {
            if (abs(j - last) >= 2)
                res += f[i + 1][j];
        }
 
        if (abs(last - x) >= 2)
            last = x;
        else
            break;
 
        if (!i)
            res++;
    }
 
    for (int i = 1; i <= nums.size() - 1; i++)
        for (int j = 1; j <= 9; j++)
            res += f[i][j];
 
    return res;
}

3.5.3 求满足条件的第I个数是什么

3.5.3.1 比特串

这个集合内的元素满足:

  • 按照二进制表示从小到大排序。
  • 每一个元素的 位字符中,字符 的数量均不超过 。
  • 所有满足上一条件的 位 串保证都在集合之中。

现在请你求出这个集合中的第 个元素是多少。

既然要求第I个数, 只有当前位选1的方案数等于I时, 将当前位输出(即选0时方案数小于I), 否则输出0, 这样可以贪心地求出最小。

设DP数组: f[i][j] 长度为i, 当前最多可以用j个1

先求出全排列数组, 之后预处理f数组。预处理时, j枚举应当超出i的限制, 最多为n, 因为状态定义的是最多可以用。第三层循环枚举当前选择的1个数k, 其大于i也没事, 全排列数组中对于这样的会初始化为0。

DP求解时从高到低位枚举, 若当前选0时方案数小于I, 说明当前需要选1, 即 f[i-1][L] < I 时, cout << 1, 然后再将 I-=f[i-1][L], 因为当前选1之后, 下一位能选的方案数就不能再是I了, 还要把当前能用的1个数减1, L--。

int main()
{
    LL n,l,m;
    cin >> n >> m >> l;
    
    c[0][0] = 1;
    for(int i = 1; i <= n; i++)
        for(int j = 0; j <= i; j++)
            if(!j) c[i][j] = 1;
            else c[i][j] = c[i- 1][j] + c[i-1][j-1];
    
    for(int i = 0; i <= n; i++) // 第i位
        for(int j = 0; j <= n; j++) // 当前位后面最多选j个1
            for(int k = 0; k <= j; k++) // 选1
                f[i][j] += c[i][k];
    
    for(int i = n; i >= 1; i--)
    {
        if(f[i-1][m] < l)
        {
            cout << 1;
            l -= f[i-1][m];
            m--;
        }else cout << 0;
        
    }
                
    
    return 0;
}

3.5.3.2 不含4的递增序列找第k个数

void init()
{
    for (int i = 0; i <= 9; i++)
        if (i != 4)
            f[1][i] = 1;
    for (int i = 2; i <= N - 1; i++)
    {
        for (int j = 0; j <= 9; j++)
        {
            if (j == 4)
                continue;
            for (int k = 0; k <= 9; k++)
                f[i][j] += f[i - 1][k];
        }
    }
}
 
ll dp(ll x)
{
    if (!x)
        return 0;
 
    vector<int> nums;
    while (x)
        nums.push_back(x % 10), x /= 10;
 
    ll res = 0;
    for (int i = nums.size() - 1; i >= 0; i--)
    {
        int x = nums[i];
        for (int j = (i == nums.size() - 1); j < x; j++)
            res += f[i + 1][j];
        if (x == 4)
            break;
        if (!i)
            res++;
    }
    for (int i = 1; i <= nums.size() - 1; i++)
        for (int j = 1; j <= 9; j++)
            res += f[i][j];
    return res;
}
 
int main()
{
    init();
 
    int T;
    cin >> T;
    for (int i = 1; i <= 15; i++)
        cout << dp(i) << " ";
    cout << endl;
    while (T--)
    {
        ll k;
        cin >> k;
        ll l = -1, r = 1e13;
        while (l != r - 1)
        {
            ll mid = l + r >> 1;
            if (dp(mid) < k)
                l = mid;
            else
                r = mid;
        }
        cout << r << endl;
    }
    return 0;
}

也可以用进制转换:

int main()
{
 
    cin.tie(0);
    cout.tie(0);
    ios::sync_with_stdio(0);
    string s = "012356789";
    int T;
    cin >> T;
    while (T--)
    {
 
        LL k;
        cin >> k;
        cnt = 0;
        while (k)
            a[cnt++] = s[k % 9] - '0', k /= 9;
        for (int i = cnt - 1; i >= 0; i--)
            cout << a[i];
        cout << endl;
    }
}

3.6 斜率优化DP

有 个任务排成一个序列在一台机器上等待执行,它们的顺序不得改变。

机器会把这 个任务分成若干批,每一批包含连续的若干个任务。

从时刻 开始,任务被分批加工,执行第 个任务所需的时间是 。

另外,在每批任务开始前,机器需要 的启动时间,故执行一批任务所需的时间是启动时间 加上每个任务所需时间之和。

一个任务执行后,将在机器中稍作等待,直至该批任务全部执行完毕。

也就是说,同一批任务将在同一时刻完成。

每个任务的费用是它的完成时刻乘以一个费用系数 。

请为机器规划一个分组方案,使得总费用最小。

根据题目要求可得有一种划分方式得到的代价为:

image-20230503182829998

每个划分的代价可变的部分取决于该区间结束时的时间time, 同一段任务可能因为先后顺序不同而代价不同。

对于启动时间S, 在任意时刻启动时, 都会对后面所有的任务段造成影响, 让他们的 time + S, 可以直接提前算掉当前任务所带来的花费, 即:

状态表示:f[i]前i个任务处理完的所有方案的集合, 值为最小代价

得出了转移方程f[i] = f[j] + sumT[i]*(sumC[i]-sumC[j])+S*(sumC[n]-sumC[j]) 初始时总费用为0。

3.6.1 N为3e5时

当状态计算到i时, 我们需要枚举的只有j, 也就是说只有j是变量。

但如果有其他性质的话可以更加优化:

  • 题目中T都为整数, 故sumT[i]*S即斜率k是单调递增的
  • i从低到高枚举时, k单调递增, 即每个新直线不会用到之前用过的最小点 因此, 当用一个队列维护凸包下边界点时: , 查询的时候可以把队头小于当前斜率的点全部删掉。 插入的时候把队尾不在凸包上的点全删掉。 这个过程其实就是Graham’s Scan算法中下边界的求法。原算法开始时有一步是要对所有点关于x从小到大排序然后枚举, 这里则因为sumC[i]是递增的故省去。
int main()
{
    cin >> n >> s;
    for(int i = 1; i <= n; i++)
    {
        cin >> t[i] >> c[i];
        t[i] += t[i - 1];
        c[i] += c[i - 1];
    }
    
    int hh = 0, tt = 0;
    q[0] = 0;
    for(int i = 1; i <= n; i++)
    {
        while(hh < tt && f[q[hh + 1]] - f[q[hh]] <= (c[q[hh + 1]] - c[q[hh]]) * (t[i] + s)) hh++;
        int j = q[hh];
        f[i] = f[j] - c[j]*(t[i] + s) + t[i]*c[i] + s*c[n];
        while(hh < tt && (f[q[tt]] - f[q[tt - 1]]) * (c[i] - c[q[tt]]) >= (f[i] - f[q[tt]]) * (c[q[tt]] - c[q[tt - 1]])) tt--;
        q[++tt] = i;
    }
    cout << f[n] << endl;
    return 0;
    
}

3.6.2 当T可以为负数时

,
,

#include <iostream>
using namespace std;
const int N = 3e5 + 10;
 
typedef long long LL;
LL f[N];
LL c[N], t[N];
int q[N];
int n, s;
 
int main()
{
    cin >> n >> s;
    for (int i = 1; i <= n; i++)
    {
        cin >> t[i] >> c[i];
        t[i] += t[i - 1];
        c[i] += c[i - 1];
    }
 
    int hh = 0, tt = 0;
    q[0] = 0;
    for (int i = 1; i <= n; i++)
    {
        int l = hh - 1, r = tt + 1;
        while (l != r - 1)
        {
            int mid = l + r >> 1;
            if (f[q[mid + 1]] - f[q[mid]] >= (t[i] + s) * (c[q[mid + 1]] - c[q[mid]]))
                r = mid;
            else
                l = mid;
        }
        int j = q[r];
        f[i] = f[j] - c[j] * (t[i] + s) + t[i] * c[i] + s * c[n];
        while (hh < tt && (double)(f[q[tt]] - f[q[tt - 1]]) * (c[i] - c[q[tt]]) >= (double)(f[i] - f[q[tt]]) * (c[q[tt]] - c[q[tt - 1]]))
            tt--;
        q[++tt] = i;
    }
    cout << f[n] << endl;
    return 0;
}

3.6.3 应用:运输小猫

小 是农场主,他养了 只猫,雇了 位饲养员。

农场中有一条笔直的路,路边有 座山,从 到 编号。

第 座山与第 座山之间的距离为 。

饲养员都住在 号山。

有一天,猫出去玩。

第 只猫去 号山玩,玩到时刻 停止,然后在原地等饲养员来接。

饲养员们必须回收所有的猫。

每个饲养员沿着路从 号山走到 号山,把各座山上已经在等待的猫全部接走。

饲养员在路上行走需要时间,速度为 米/单位时间。

饲养员在每座山上接猫的时间可以忽略,可以携带的猫的数量为无穷大。

例如有两座相距为 的山,一只猫在 号山玩,玩到时刻 开始等待。

如果饲养员从 号山在时刻 或 出发,那么他可以接到猫,猫的等待时间为 或 。

而如果他于时刻 出发,那么他将于时刻 经过 号山,不能接到当时仍在玩的猫。

你的任务是规划每个饲养员从 号山出发的时间,使得所有猫等待时间的总和尽量小。

饲养员出发的时间可以为负。

image-20230503183301694

image-20230503183308833

const int N = 1e5 + 10;
typedef long long LL;
int n,m,p;
LL a[N]; // 最小等待时间
LL d[N], s[N];
LL f[110][N];
int q[N];
 
LL get_y(int j, int k)
{
    return f[j - 1][k] + s[k];
}
 
int main()
{
    ios::sync_with_stdio(false);cin.tie(0);
    cin >> n >> m >> p;
    for(int i = 2 ; i <= n; i++)
    {
        cin >> d[i];
        d[i] += d[i - 1];
    }
    for(int i = 1; i <= m; i++)
    {
        LL h, t;
        cin >> h >> t;
        a[i] = t - d[h];
    }
    sort(a + 1, a + m + 1);
    for(int i = 1; i <= m; i++) s[i] = s[i - 1] + a[i];
    
    memset(f, 0x3f, sizeof f);
    for(int i = 0; i <= p; i++) f[i][0] = 0;
    
    for(int j = 1; j <= p; j++)
    {
        int hh = 0, tt = 0;
        q[0] = 0;
        for(int i = 1; i <= m; i++)
        {
            while(hh < tt && (get_y(j,q[hh + 1]) - get_y(j,q[hh])) <=  a[i] * (q[hh + 1] - q[hh])) hh++;
            int k = q[hh];
            f[j][i] = f[j - 1][k] + s[k] - a[i]*k + a[i]*i - s[i];
            while(hh < tt && (get_y(j, q[tt]) - get_y(j, q[tt - 1])) * (i - q[tt])
                                >= (get_y(j, i) - get_y(j, q[tt])) * (q[tt] - q[tt - 1])) tt--;
            q[++ tt] = i;
        }
    }
    cout << f[p][m] << endl;
    return 0;
}

3.7 树形DP

3.7.1 树的直径

const int N = 1e4 +10, M = 2*N;
int h[N], e[M], ne[M], w[M], idx;
int n,res;
 
void add(int a, int b, int c)
{
    w[idx] = c, e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
 
int dfs(int u, int fa)
{
     int dist = 0;
     int d1 = 0, d2 = 0;
     
     for(int i = h[u]; ~i; i = ne[i])
     {
         int j = e[i];
         if(j == fa) continue;
         int d = dfs(j, u) + w[i];
         dist = max(dist, d);
         
         if(d >= d1) d2 = d1, d1 = d;
         else if(d > d2) d2 = d;
     }
     
     res = max(res, d1 + d2);
     return dist;
}
 
int main()
{
    cin >> n;
    memset(h, -1, sizeof h);
    for(int i = 0; i < n-1; ++i)
    {
        int a,b,c;
        cin >> a >> b >> c;
        add(a,b,c);
        add(b,a,c);
    }
    
    dfs(1, -1);
    cout << res << endl;
    
    return 0;
}

3.7.1.1 应用:数字转换

如果一个数 x 的约数(不包括他本身)的和 y 比他本身小,那么 x 可以变成 y , y 也可以变成 x 。例如 4 可以变为 3 , 1 可以变为 7 。限定所有数字变换在不超过 n 的正整数范围内进行,求不断进行数字变换且不出现重复数字的最多变换步数。

对于数字6, 它的约数为 1 2 3, 总和为6, 无法变换。 对于数字4, 它的约数为 1 2, 总和为3, 故可以 4 变换 3。

显然对于每个数字, 能变换过去的约数只有一个, 因此可以看做只有一个父节点。问题可以转化为图论树模型:image-20230503183559195 所以我们要求的就是该树的最长路径(直径)。 按照该模板题的方法求即可。树的最长路径 求一个数的所有约数可以使用试除法试除法(sqrt(n))。注意减去该数本身。 总算法复杂度为 求约数还可以用标记法, 即求该数是那些数的约数, 类似于埃氏筛 这种的复杂度是调和级数, 复杂度为

const int N = 5e4 + 10, M = 2 * N;
int h[N], e[M], ne[M], idx;
int n;
int ans;
int sum[N]; // i数的约数之和
 
// 试除法
int get(int i)
{
    int res = 0;
    for (int j = 1; j <= i / j; j++)
        if (i % j == 0)
        {
            res += j;
            if (j != i / j)
                res += i / j;
        }
    res -= i;
    return res;
}
 
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
 
int dfs(int u, int fa)
{
    int dist = 0;
    int d1 = 0, d2 = 0;
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (j == fa)
            continue;
 
        int d = dfs(j, u) + 1;
        dist = max(dist, d);
        if (d >= d1)
            d2 = d1, d1 = d;
        else if (d > d2)
            d2 = d;
    }
    ans = max(ans, d1 + d2);
    return dist;
}
 
int main()
{
    FasterIO;
    cin >> n;
	for(int i = 1; i <= n; i++)
		for(int j = 2; j <= n / i; j++)
			sum[i * j] += i;
    memset(h, -1, sizeof h);
    for (int i = 1; i <= n; i++)
    {
        int b = sum[i];
        if (i > b)
            add(i, b), add(b, i);
    }
    dfs(1, -1);
    cout << ans << endl;
    return 0;
}

3.7.2 树的中心

const int N = 1e4 + 10, M = 2 * N, INF = 0x3f3f3f3f;
int h[N], e[M], ne[M], w[M], idx;
int n;
int d1[N], d2[N], p1[N], p2[N], up[N]; // p1, p2记录最大值和次大值的路径
 
void add(int a, int b,int c)
{
    e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
 
int dfs_d(int u, int fa)
{
    d1[u] = d2[u] = -INF;
    for(int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if(j == fa) continue;
        int d = dfs_d(j, u) + w[i];
        if(d >= d1[u])
        {
            d2[u] = d1[u];
            p2[u] = p1[u];
            d1[u] = d;
            p1[u] = j;
        }
        else if(d > d2[u])
        {
            d2[u] = d;
            p2[u] = j;
        }
    }
    
    if(d1[u] == -INF) d1[u] = d2[u] = 0;
    
    return d1[u];
}
 
void dfs_u(int u, int fa)
{
    for(int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if(j == fa) continue;
        
        if(p1[u] == j) up[j] = max(up[u], d2[u]) + w[i];
        else up[j] = max(up[u], d1[u]) + w[i];
        
        dfs_u(j, u);
    }
}
 
int main()
{
    cin >> n;
    memset(h, -1, sizeof h);
    for(int i = 1; i < n; i++)
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(a,b,c), add(b,a,c);
    }
    
    dfs_d(1,-1);
    dfs_u(1,-1);
    
    int res = INF;
    for(int i = 1; i <= n; i++)
    {
        res = min(max(up[i], d1[i]), res);
    }
    cout << res << endl;
    
    return 0;
}

3.7.3 从根开始的有依赖背包问题

有一棵二叉苹果树,如果树枝有分叉,一定是分两叉,即没有只有一个儿子的节点。这棵树共 个节点,标号 至 ,树根编号一定为 。

我们用一根树枝两端连接的节点编号描述一根树枝的位置。一棵有四根树枝的苹果树,因为树枝太多了,需要剪枝。但是一些树枝上长有苹果,给定需要保留的树枝数量,求最多能留住多少苹果。

需要在树中找到包含根节点且长度为Q的一条路径, 其包含的苹果数量最大

且根据剪枝的性质, 如果当前树枝需要保留, 那么其父节点的树枝也一定需要保留, 也就是说存在依赖性。 把树枝权重放到点上, 树枝数量当做背包容量, 每个物品重量为1, 可以转化为背包问题中的有依赖背包问题。复杂度为 可以通过。

状态表示:f[i][j] 以i为根的节点的子树中选择j个树枝

const int N = 110, M = N * 2;
int h[N], e[M], ne[M], w[M], idx;
int n, Q;
int f[N][N];
 
void add(int a, int b, int c)
{
    e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
 
void dfs(int u, int fa)
{
    for (int i = h[u]; ~i; i = ne[i])
    {
        int t = e[i];
        if (t == fa)
            continue;
        dfs(t, u);
        for (int j = Q; j >= 0; j--)
            for (int k = 0; k < j; k++)
                f[u][j] = max(f[u][j], f[t][k] + f[u][j - k - 1] + w[i]);
    }
}
 
int main()
{
    cin >> n >> Q;
    memset(h, -1, sizeof h);
    for (int i = 1; i < n; i++)
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c), add(b, a, c);
    }
 
    dfs(1, -1);
    cout << f[1][Q] << endl;
 
    return 0;
}

3.7.4 树形DP+状态机DP

Bob 喜欢玩电脑游戏,特别是战略游戏。但是他经常无法找到快速玩过游戏的方法。现在他有个问题。

现在他有座古城堡,古城堡的路形成一棵树。他要在这棵树的节点上放置最少数目的士兵,使得这些士兵能够瞭望到所有的路。

注意:某个士兵在一个节点上时,与该节点相连的所有边都将能被瞭望到。

请你编一个程序,给定一棵树,帮 Bob 计算出他最少要放置的士兵数。

和上司的舞会那道题很像, 之前是每条边最多选择一个, 求最大权值和, 是最大独立集问题。 而这一题就是每条边最少选择一个, 求最小权值和。

每个点被观察到时存在三种情况

  • 父节点有士兵
  • 该点有士兵
  • 子节点有士兵

沿用状态机DP的思想, 状态表示为:f[i][2] 以 i 为根的子树所有覆盖的方案中, 最少需要的士兵数。f[i][0] 代表根节点无士兵, f[i][1]代表根节点有士兵。

状态转移: 只考虑当前节点和子节点, 显然f[i][0]可以从子节点有士兵转移, f[i][1]可以从子节点无士兵转移。 f[i][0] += f[j][1] f[i][1] += min(f[j][1], f[j][0])

初始所有点为: f[i][0] = 0 f[i][1] = 1

const int N = 2e3;
int h[N], e[N], ne[N], idx;
int n;
int f[N][2];
bool st[N];
 
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
 
void dfs(int u)
{
 
    f[u][0] = 0;
    f[u][1] = 1;
    for(int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
 
        dfs(j);
        f[u][0] += f[j][1];
        f[u][1] += min(f[j][1], f[j][0]);
    }
}
 
int main()
{
    while(scanf("%d", &n) != EOF)
    {
        memset(h,-1,sizeof h);
        idx = 0;
        memset(st, false, sizeof st);
        for(int i = 0; i < n; i++)
        {
            int a,m;
            scanf("%d:(%d)", &a, &m);
            for(int i = 0; i < m; i++)
            {
                int b;
                scanf("%d", &b);
                add(a,b);
                st[b] = true;
                
            }
        }
        int root = 0;
        while(st[root]) root++;
        dfs(root);
        printf("%d\n", min(f[root][1], f[root][0]));
        
    }
    return 0;
}

3.8 状态压缩DP

3.8.1 矩阵填图形方案数

#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 13, M = 1 << N;
typedef long long LL;
 
LL f[N][M];
int state[M];
vector<int> st[M];
int n,m;
 
bool check(int x)
{
    int cnt = 0;
    for(int i = 0; i < n;i ++)
    {  
       if(x >> i & 1)
       {
           if(cnt & 1)
           {
               return false;
           }
           else cnt = 0;
       }
       else cnt++;
    }
    if(cnt & 1) return false;
    return true;
}
 
int main()
{
    while(cin >> n >> m, n || m)
    {
        memset(f, 0, sizeof f);
        f[0][0] = 1;
        for(int i = 1; i <= m;i ++)
        {
            for(int j = 0; j < 1 << n; j++)
            {
                for(int k = 0; k < 1 << n; k++)
                {
                    if(((j & k) == 0)&& check(j | k))
                    {
                        f[i][j] += f[i - 1][k];
                    }
                }
            }
        }
        cout << f[m][0] << endl;
    }
    return 0;
}

3.8.2 最短Hamilton路径, 旅行商问题

给定一张 n 个点的带权无向图,点从 0∼n−1 标号,求起点 0 到终点 n−1 的最短 Hamilton 路径。

Hamilton 路径的定义是从 0 到 n−1 不重不漏地经过每个点恰好一次。

图的深度优先搜索可以实现, 保存当前路径并且判断是否不重不漏。 但时间复杂度起码为 20!, 不能用暴力方法。

那么状态表示 f[i][j] 就可以, i 是二进制表示有几个数被选上, j 为结尾数

怎么转移呢? 假设 f[i][j] 由 f[state_k][k] 转移 state_k 需要保证 j 没有被选上, 然后加上 k → j 的权值 即 f[i][j] = f[state_k][k] + w[k][j]

怎么枚举能转移到 j 的这个 state_k 呢? 首先 j 要已经被选上, 即 i 的 第 j 位 为 1 那么 k 的状态就需要还没选上 j, 即 state_k = i - (1 << j) 接着该状态需要包含 k, 即 (state_k >> k) & 1 然后再加上权值即可.

const int N = 20, M = 1 << N;
int f[M][N];
int w[N][N];
 
int n;
int main()
{
    cin >> n;
    for(int i = 0; i < n;i ++)
        for(int j = 0; j < n; j ++)
        {
            scanf("%d", &w[i][j]);
        }
    memset(f, 0x3f, sizeof f);
    f[1][0] = 0;
    for(int i = 0; i < 1 << n; i++)
    {
        for(int j = 0; j < n; j++)
        {
            if(i >> j & 1)
            {
                for(int k = 0; k < n; k++)
                {
                    if(i - (1 << j) >> k & 1)
                        f[i][j] = min(f[i][j], f[i - (1 << j)][k] + w[k][j]);
                }
            }
        }
    }
    cout << f[(1 << n) - 1][n - 1] << endl;
    return 0;
}

3.8.3 放置问题—相邻8个格子不能放

在nxn的棋盘上放k个国王,国王可攻击相邻的8个格子,求使它们无法互相攻击的方案总数。

const int N = 12, M = 1 << 10, K = 110;
long long f[N][K][M];
int n, k;
vector<int> state;
vector<int> head[M];
int cnt[M];
 
bool check(int s)
{
    if (((s << 1) & s) && ((s >> 1) & s))
        return false;
    else
        return true;
}
bool check(int a, int b)
{
    if ((a & b) == 0 && check(a | b))
        return true;
    return false;
}
 
int getSum(int s)
{
    return __builtin_popcount(s);
}
 
int main()
{
    cin >> n >> k;
    for (int i = 0; i < 1 << n; i++)
    {
        if (check(i))
        {
            state.push_back(i);
            cnt[i] = __builtin_popcount(i);
        }
    }
 
    for (int i = 0; i < state.size(); i++)
    {
        for (int j = 0; j < state.size(); j++)
        {
            int a = state[i], b = state[j];
            if (check(a, b))
                head[a].push_back(b);
        }
    }
 
    f[0][0][0] = 1;
    for (int i = 1; i <= n + 1; i++)
    {
        for (int j = 0; j <= k; j++)
        {
            for (auto a : state)
            {
                int c = cnt[a];
                for (auto b : head[a])
                {
                    if (j >= c)
                        f[i][j][a] += f[i - 1][j - c][b];
                }
            }
        }
    }
 
    cout << f[n + 1][k][0] << endl;
    return 0;
}

3.8.4 放置问题—相邻不能放且固定位置不能放

农夫约翰的土地由个小方格组成,现在他要在土地里种植玉米。 非常遗憾,部分土地是不育的,无法种植。 而且,相邻的土地不能同时种植玉米,也就是说种植玉米的所有方格之间都不会有公共边缘。 现在给定土地的大小,请你求出共有多少种种植方法。 土地上什么都不种也算—种方法。

const int N = 14, M = 1 << 12, mod = 1e8;
 
int n,m;
int g[N];
vector<int> state;
vector<int> head[M];
int f[N][M];
 
inline bool check(int s)
{
    if (((s << 1) & s) && ((s >> 1) & s))
        return false;
    else
        return true;
}
 
int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        for(int j = 0; j < m; j++)
        {
            int t;
            cin >> t;
            g[i] += !t << j;
        }
    }
    
    for(int i = 0; i < 1 << m; i++)
    {
        if(check(i))
            state.push_back(i);
    }
    
    for(int i = 0; i <state.size(); i++)
        for(int j = 0; j < state.size();j ++)
        {
            if(!(state[i] & state[j]))
                head[i].push_back(j);
        }
    
    f[0][0] = 1;
    for(int i = 1; i <= n + 1; i++)
    {
        for(int a = 0; a < state.size(); a++)
        {
            for(int b : head[a])
            {
                if(state[a] & g[i]) continue;
                f[i][a] = (f[i][a] + f[i - 1][b]) % mod;
            }
        }
    }
    cout << f[n + 1][0] << endl;
    return 0;
}

3.8.5 放置问题—影响范围扩大到i-2行

状态表示:f[i][j][k] 只安排前i行, 其中第i行的状态为j, 第i-1行的状态为k, 所有情况的安排数量最大值。

状态计算: 设第i行状态为a, 第i-1行状态为b, 第i-2行状态为c, 他们需要满足的条件为:

  • 内部无相邻间隔小于两格的
  • 上下行之间不能有相同列的
  • 士兵不能在山地上
const int N = 110, M = 1 << 10;
int g[N];
int f[2][M][M]; // 滚动数组优化
vector<int> state;
vector<int> head[M];
int n, m;
 
inline bool check(int s)
{
    if (((s >> 1 & s) || (s >> 2 & s) || (s << 1 & s) || (s << 2 & s)) == 0)
        return true;
    return false;
}
int count(int i)
{
    return __builtin_popcount(i);
}
 
int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        for (char c; j < m && cin >> c; ++ j)
            g[i] += (c == 'H') << j;
 
    for (int i = 0; i < 1 << m; i++)
    {
        if (check(i))
            state.push_back(i);
    }
 
    for (int i = 0; i < state.size(); i++)
        for (int j = 0; j < state.size(); j++)
        {
            if (!(state[i] & state[j]))
                head[i].push_back(j);
        }
 
    for (int i = 1; i <= n + 2; i++)
    {
        for (int a = 0; a < state.size(); a++)
        {
            for (int b = 0; b < state.size(); b++)
            {
                for (int c = 0; c < state.size(); c++)
                {
                    if (g[i] & state[a] | g[i - 1] & state[b])
                        continue;
                    if (state[a] & state[b] | state[a] & state[c] | state[b] & state[c])
                        continue;
                    f[i & 1][a][b] = max(f[i & 1][a][b], f[(i - 1) & 1][b][c] + count(state[a]));
                }
            }
        }
    }
 
    cout << f[(n + 2) & 1][0][0] << endl;
    return 0;
}

3.8.6 二次曲线点覆盖问题

状态表示: f[state] 当前所有点的覆盖状态为state, 所用的抛物线数量(state是二进制压缩储存的点集覆盖状态) 状态计算: f[new_state] = min(f[state] + 1) new_state即为用新抛物线覆盖之后的点集覆盖情况

const int N = 20, M = 1 << 18, INF = 0x3f3f3f3f;
const double eps = 1e-6;
typedef pair<double, double> PDD;
int f[M];
PDD point[N];
int path[N][N];
int n,m;
 
int cmp(double a, double b)
{
    if(fabs(a - b) < eps) return 0;
    if(a > b) return 1;
    return -1;
}
 
int main()
{
    int T;
    cin >> T;
    while(T--)
    {
        memset(f, 0x3f, sizeof f);
        memset(path, 0, sizeof path);
        cin >> n >> m;
        for(int i = 0; i < n; i++)
            cin >> point[i].x >> point[i].y;
        for(int i = 0; i < n; i++)
        {
            path[i][i] = 1 << i;
            for(int j = 0; j < n; j++)
            {
                double x1 = point[i].x, y1 = point[i].y;
                double x2 = point[j].x, y2 = point[j].y;
                if(!cmp(x1,x2)) continue;
                double a = (y1/x1 - y2/x2)/(x1-x2);
                double b = y1/x1 - a*x1;
                if(cmp(a,0.0) >= 0) continue;
                path[i][j] = (1 << i) + (1 << j);
                
                for(int k = 0; k < n; k++)
                {
                    x1 = point[k].x, y1 = point[k].y;
                    if(k != i && k != j && !cmp(a*x1*x1 + b*x1, y1))
                        path[i][j] += 1 << k;
                }
            }
        }
        
        int res = INF;
        f[0] = 0;
        for(int i = 0; i + 1 < 1 << n; i++)
        {
            int  t = -1;
            for(int k = 0; k < n; k++)
                if(!(i >> k & 1))
                {
                    t = k;
                    break;
                }
            for(int k = 0; k < n; k++)
            {
                int new_state = path[t][k] | i;
                f[new_state] = min(f[new_state], f[i] + 1);
            }
        }
        cout << f[(1 << n) - 1] << endl;
        
        
    }
    return 0;
}

3.9 状态机DP

3.9.1 按时间顺序

给定一个长度为 的数组,数组中的第 个数字表示一个给定股票在第 天的价格。

设计一个算法来计算你所能获取的最大利润,在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票)

  • 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。一次买入卖出合为一笔交易。
  • 卖出股票后,你无法在第二天买入股票(即冷冻期为1天)。

状态表示: f[i][3] 第i天, 当前有无股票 状态计算: f[i][0] = max(f[i - 1][j], f[i - 1][2] - w[i]) f[i][1] = f[i][0] + w[i] f[i][2] = max(f[i - 1][1], f[i - 1][2])

const int N = 5e3 + 10;
int maxProfit(vector<int>& prices) {
        int f[N][3];
        memset(f, -0x3f, sizeof f);
        f[0][2] = 0;
        for(int i = 0; i < prices.size(); i++)
        {
            f[i][0] = max(f[i - 1 < 0?0:i-1][0], f[i - 1 < 0?0:i-1][2] - prices[i]);
            f[i][1] = f[i][0] + prices[i];
            f[i][2] = max(f[i - 1 < 0?0:i-1][1], f[i - 1 < 0?0:i-1][2]);
        }
        return max(f[prices.size()-1][1], f[prices.size()-1][2]);
    }
 

3.9.2 按长度枚举 KMP状态机

你现在需要设计一个密码,需要满足; 的长度是 只包含小写英文字母 不包含子串

例如: 和 是 的子串, 不是 的子串。 请问共有多少种不同的密码满足要求? 由于答案会非常大,请输出答案模的余数。

KMP的匹配过程其实就是一个状态自动机, 在j位置上如果不能调到j+1, 则调到 ne[j]。即长度为j时可以转移到长度为j+1或长度为ne[j]。 设 为可行状态的长度为j时的方案 可得: 在结尾时将所有长度情况的可行状态的方案数加起来, 就是总方案数。 而对于每个i, 都有26种对应的可行状态, 因此状态定义为: f[i][j]: 已经确定i长度, 且其匹配的s串位置为j, 要枚举i+1的字符。 此时所谓的确定的i长度p串, 匹配的j长度s串都是抽象的, 但一定存在。

初始状态:f[0][0] = 1 最终答案:sum(f[n][i]) i = (0,1,2,...,m-1)

状态更新: 令 u = j, 维持状态恒定 当p[i + 1] != s[u + 1]时, 执行u = ne[u], 直到相等为止 相等后将u++ 此时说明f[i + 1][u] 可以从 f[i][j] 也就是当前状态 转移而来。 则将f[i + 1][u] += f[i][j]

const int N = 55, mod = 1e9 + 7;
int n, m;
char T[N];
int ne[N];
long long ans = 0;
int f[N][N];
 
int main()
{
    cin >> n >> T + 1;
    m = strlen(T + 1);
    for (int i = 2, j = 0; i <= m; i++)
    {
        while (j && T[i] != T[j + 1])
            j = ne[j];
        if (T[i] == T[j + 1])
            j++;
        ne[i] = j;
    }
 
    f[0][0] = 1;
    for (int i = 0; i < n; i++)
    {
        for (int j = 0; j < i && j < m; j++)
        {
            for (char c = 'a'; c <= 'z'; c++)
            {
                int u = j;
                while (u && T[u + 1] != c)
                    u = ne[u];
                if (T[u + 1] == c)
                    u++;
                if (u < m)
                    f[i + 1][u] = (f[i + 1][u] + f[i][j]) % mod;
            }
        }
    }
    for (int i = 0; i < m; i++)
        ans = (ans + f[n][i]) % mod;
 
    cout << ans << endl;
    return 0;
}

4 图论

4.1 tarjan 连通分量

4.1.1 有向图

4.1.1.1 最长链权值

给定一个 个点 条边有向图,每个点有一个权值,求一条路径,使路径经过的点权值之和最大。你只需要求出这个权值和。

允许多次经过一条边或者一个点,但是,重复经过的点,权值只计算一次。

const int N = 1e4 + 10, M = 2e5 + 10;
int n, m;
int value[N];
 
int h[N], hs[N], e[M], ne[M], w[M], idx;
 
int dfn[N], low[N], ts;
int stk[N], top;
bool instk[N];
int id[N], scnt, sccsize[N];
int f[N]; // dp最大点权之和
 
void add(int h[], int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
 
void tarjan(int u)
{
    dfn[u] = low[u] = ++ts;
    stk[++top] = u, instk[u] = true;
 
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (!dfn[j])
        {
            tarjan(j);
            low[u] = min(low[u], low[j]);
        }
        else if (instk[j])
            low[u] = min(low[u], dfn[j]);
    }
 
    if (low[u] == dfn[u])
    {
        ++scnt;
        int y;
        do
        {
            y = stk[top--];
            instk[y] = false;
            id[y] = scnt;
            sccsize[scnt] += value[y];
        } while (y != u);
    }
}
 
int main()
{
    mem(h, -1);
    mem(hs, -1);
    FasterIO;
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        cin >> value[i];
    while (m--)
    {
        int a, b;
        cin >> a >> b;
        add(h, a, b, value[b]);
    }
 
    for (int i = 1; i <= n; i++)
        if (!dfn[i])
            tarjan(i);
    //缩点建边
    for (int i = 1; i <= n; i++)
        for (int j = h[i]; ~j; j = ne[j])
        {
            int k = e[j];
            int a = id[i], b = id[k];
            if (a != b)
            {
                add(hs, a, b, sccsize[b]);
            }
        }
    // 1-scnt 新图 做dp求最长路径
    int res = 0;
    for (int i = scnt; i >= 1; i--)
    {
        if (!f[i])
            f[i] = sccsize[i];
        for (int j = hs[i]; ~j; j = ne[j])
        {
            int k = e[j];
            f[k] = max(f[k], f[i] + sccsize[k]);
        }
        res = max(f[i], res);
    }
    cout << res << endl;
    return 0;
}

4.1.1.2 O(N) 求差分约束

#define size Size
const int N = 1e5 + 10, M = 6e5 + 10;
typedef long long LL;
int h[N], hs[N], e[M], ne[M], w[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
bool in_stk[N];
int id[N], scc_cnt, size[N];
int n,m;
LL dist[N];
 
void add(int h[], int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
 
void tarjan(int u)
{
    low[u] = dfn[u] = timestamp++;
    stk[++top] = u, in_stk[u] = true;
    
    for(int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if(!dfn[j])
        {
            tarjan(j);
            low[u] = min(low[u], low[j]);
        }
        else if(in_stk[j]) low[u] = min(low[u], dfn[j]);
    }
    
    if(dfn[u] == low[u])
    {
        ++scc_cnt;
        int y;
        do {
            y = stk[top--];
            in_stk[y] = false;
            id[y] = scc_cnt;
            size[scc_cnt]++;
        }while(y != u);
    }
}
 
int main()
{
    memset(h,-1,sizeof h);
    memset(hs, -1, sizeof hs);
    cin >> n >> m;
    while(m--)
    {
        int a, b, c;
        cin >> c >> a >>  b;
        if(c == 1) add(h,a,b,0), add(h,b,a,0);
        else if(c == 2) add(h,a, b, 1);
        else if(c == 3) add(h,b,a,0);
        else if(c == 4) add(h,b, a, 1);
        else add(h,a,b,0);
    }
    for(int i = 1; i <= n; i++) add(h,0,i,1);
    
    tarjan(0);
    bool flag = true;
    for(int i = 0; i <= n; i++)
    {
        for(int j = h[i]; ~j; j = ne[j])
        {
            int k = e[j];
            int a = id[i], b = id[k];
            if(a == b)
            {
                if(w[j] > 0)
                {
                    flag = false;
                    break;
                }
            }
            else add(hs, a,b, w[j]);
        }
        if(!flag) break;
    }
    
    if(!flag) cout << -1 << endl;
    else 
    {
        LL res = 0;
        for(int i = scc_cnt; i; i--)
        {
            for(int j = hs[i]; ~j; j = ne[j])
            {
                int k = e[j];
                dist[k] = max(dist[k], dist[i] + w[j]);
            }
        }
        for(int i = 1; i <= scc_cnt; i++)
            res += (LL)dist[i]*size[i];
        cout << res << endl;
    }
        
    
    return 0;
}

4.1.1.3 最长链数量

const int N = 1e5 + 10, M = 2e6 + 10;
int n, m, mod;
// tarjan
int dfn[N], low[N], timestamp;
int stk[N], top;
bool in_stk[N];
int id[N], size[N], scc_cnt;
 
// 最大连通图点数量, 最大连通图数量
int f[N], g[N]; 
 
// 存图
int h[N], hs[N], e[M], ne[M], idx;
 
void add(int h[], int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
 
void tarjan(int u)
{
    dfn[u] = low[u] = ++timestamp;
    stk[++top] = u, in_stk[u] = true;
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (!dfn[j])
        {
            tarjan(j);
            low[u] = min(low[u], low[j]);
        }
        else if (in_stk[j])
            low[u] = min(low[u], dfn[j]);
    }
    if (low[u] == dfn[u])
    {
        ++scc_cnt;
        int y;
        do
        {
            y = stk[top--];
            in_stk[y] = false;
            id[y] = scc_cnt;
            size[scc_cnt]++;
        } while (y != u);
    }
}
 
int main()
{
    FasterIO;
    mem(h, -1);
    mem(hs, -1);
    cin >> n >> m >> mod;
    while (m--)
    {
        int a, b;
        cin >> a >> b;
        add(h, a, b);
    }
    for (int i = 1; i <= n; i++)
        if (!dfn[i])
            tarjan(i);
 
    unordered_set<LL> S;
    for (int i = 1; i <= n; i++)
        for (int j = h[i]; ~j; j = ne[j])
        {
            int k = e[j];
            int a = id[i], b = id[k];
            LL hash = a * 100000ll + b; // hash来去掉重边, 这里a <= 1e5, 乘上1e5然后再加上b 就不会冲突
            if (a != b && !S.count(hash))
            {
                add(hs, a, b);// 缩点建图
                S.insert(hash);
            }
        }
 
    for (int i = scc_cnt; i >= 1; i--)
    {
        if (!f[i])
        {
            f[i] = size[i];
            g[i] = 1;
        }
        for (int j = hs[i]; ~j; j = ne[j])
        {
            int k = e[j];
            if (f[k] < f[i] + size[k])
            {
                f[k] = size[k] + f[i];
                g[k] = g[i];
            }
            else if (f[k] == f[i] + size[k])
                g[k] = (g[k] + g[i]) % mod;
        }
    }
 
    int maxf = 0, sum = 0;
    for (int i = 1; i <= scc_cnt; i++)
    {
        if (f[i] > maxf)
        {
            maxf = f[i];
            sum = g[i];
        }
        else if (f[i] == maxf)
            sum = (sum + g[i]) % mod;
    }
    cout << maxf << "\n"
         << sum << "\n";
    return 0;
}

4.1.1.4 有向图加边成为强连通图

q为起点, Q为终点

显然, 类似于动态规划问题, 可以写一个转移方程: 代表起点/终点添加边的数量, 使得整个图为强连通图。 即 那么当起点先减到0时, 剩下需要添加的边数为 当终点先减到0时, 剩下需要添加的边数为 也就说, 最终答案就是 即起点数量和终点数量的最大值。

const int N = 1e5 + 10;
 
int h[N], e[N], ne[N], idx;
int stk[N], top;
bool in_stk[N];
int dfn[N], low[N], timestamp;
int n, m;
int id[N], scc_cnt;
int din[N], dout[N];
 
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
 
void tarjan(int u)
{
    dfn[u] = low[u] = ++timestamp;
    stk[++top] = u, in_stk[u] = true;
 
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (!dfn[j])
        {
            tarjan(j);
            low[u] = min(low[u], low[j]);
        }
        else if (in_stk[j])
            low[u] = min(low[u], dfn[j]);
    }
 
    if (dfn[u] == low[u])
    {
        ++scc_cnt;
        int y;
        do
        {
            y = stk[top--];
            in_stk[y] = false;
            id[y] = scc_cnt;
        } while (y != u);
    }
}
 
int main()
{
    FasterIO;
    cin >> n;
    mem(h, -1);
    for (int i = 1; i <= n; i++)
    {
        int t;
        while (cin >> t && t)
            add(i, t);
    }
 
    for (int i = 1; i <= n; i++)
        if (!dfn[i])
            tarjan(i);
 
    for (int i = 1; i <= n; i++)
    {
        for (int j = h[i]; ~j; j = ne[j])
        {
            int k = e[j];
            int a = id[i], b = id[k];
            if (a != b)
            {
                din[b]++;
                dout[a]++;
            }
        }
    }
 
    int a = 0, b = 0;
    for (int i = 1; i <= scc_cnt; i++)
    {
        if (!din[i])
            a++;
        if (!dout[i])
            b++;
    }
 
    cout << a << "\n";
    if (scc_cnt == 1)
        cout << 0 << endl;
    else
        cout << max(a, b) << endl;
 
    return 0;
}

4.1.2 无向图

4.1.2.1 点双连通分量

const int N = 1010, M = 550;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
vector<int> dcc[N];
bool cut[N];
int dcc_cnt;
int n,m, root;
 
void tarjan(int u)
{
 
    dfn[u] = low[u] = ++ timestamp;
    stk[++ top] = u;
    if(u == root && h[u] == -1)
    {
        dcc_cnt++;
        dcc[dcc_cnt].push_back(u);
        return;
    }
    
    int cnt = 0;
    for(int i = h[u]; ~i ; i = ne[i])
    {
        int j = e[i];
        if(!dfn[j])
        {
            tarjan(j);
            low[u] = min(low[u], low[j]);
            if(dfn[u] <= low[j])
            {
                cnt++;
                if(u != root || cnt > 1) cut[u] = true;
                ++dcc_cnt;
                int y;
                do {
                    y = stk[top--];
                    dcc[dcc_cnt].push_back(y);
                }while(y != j);
                dcc[dcc_cnt].push_back(u);
            }
        }
        else low[u] = min(low[u], dfn[j]);
    }
}
 
        for(int i = 0; i <= n; i++) dcc[i].clear();
        for(root = 1; root <= n; root++)
            if(!dfn[root])
                tarjan(root);

4.1.2.2 求割点

const int N = 1e4 + 10, M = 4 * N;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int n, m;
int ans, from;
 
void add(int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx++; }
 
void tarjan(int u)
{
    dfn[u] = low[u] = ++timestamp;
    int cnt = 0;
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (!dfn[j])
        {
            tarjan(j);
            low[u] = min(low[u], low[j]);
            if (dfn[u] <= low[j])
                cnt++;
        }
        else
            low[u] = min(low[u], dfn[j]);
    }
 
    if (u != from && cnt)
        cnt++;
    ans = max(ans, cnt);
}
 
int main()
{
    while (cin >> n >> m, n || m)
    {
        memset(h, -1, sizeof h);
        memset(dfn, 0, sizeof dfn);
        memset(low, 0, sizeof low);
        idx = timestamp = ans = 0;
 
        while (m--)
        {
            int a, b;
            cin >> a >> b;
            add(a, b), add(b, a);
        }
        int cnt = 0;
        for (from = 0; from < n; from++)
            if (!dfn[from])
            {
                tarjan(from);
                cnt++;
            }
        // cout << cnt << " " << ans << endl;
        cout << cnt + ans - 1 << endl;
    }
    return 0;
}

4.1.2.3 求桥&转换为边全连通图

const int N = 5e3 + 10, M = 2e5 + 10;
int h[N],e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
int id[N], dcc_cnt;
bool is_brige[M];
int d[N];
int n,m;
 
void add(int a, int b) {e[idx] = b, ne[idx] = h[a], h[a] = idx++;}
 
void tarjan(int u, int from)
{
    dfn[u] = low[u] = ++timestamp;
    stk[++top] = u;
    
    for(int i = h[u]; ~i; i = ne[i])
    {
        int j  = e[i];
        if(!dfn[j])
        {
            tarjan(j, i);
            low[u] = min(low[u], low[j]);
            if(dfn[u] < low[j])
                is_brige[i] = is_brige[i ^ 1] = true;
        }
        else if(i != (from ^ 1)) low[u] = min(low[u], dfn[j]);
    }
    
    if(dfn[u] == low[u])
    {
        ++ dcc_cnt;
        int y;
        do {
            y = stk[top--];
            id[y] = dcc_cnt;
        }while(y != u);
    }
}
 
int main()
{
    memset(h , -1, sizeof h);
    cin >> n >> m;
    while(m--)
    {
        int a, b;
        cin >> a >> b;
        add(a,b), add(b,a);
    }
    
    tarjan(1, -1);
    
    for(int i = 0; i < idx; i ++)
    {
        if(is_brige[i])
            d[id[e[i]]]++;
    }
    
    int cnt = 0;
    for(int i = 1; i <= dcc_cnt; i++)
        if(d[i] == 1)
            cnt++;
    cout << (cnt + 1) / 2 << endl; // 最少加的边数
    
    return 0;
}

4.2 二分图

4.2.1 匈牙利二分图染色求最大匹配

#include <iostream>
#include <cstring>
#include <algorithm>
#include <string>
#include <cmath>
using namespace std;
const int N = 220;
bool dist[N][N];
int match[N];
bool st[N];
int n,m;
 
bool find(int u){
    
    for(int i = 1; i <= n; i++)
        if(dist[u][i] && !st[i])
        {
            st[i] = true;
            if(!match[i] || find(match[i]))
            {
                match[i] = u;
                return true;
            }
        }
    return false;
}
 
int main()
{
    cin >> n >> m;
    while(m--)
    {
        int a, b;
        cin >> a >> b;
        dist[a][b] = true;
    }
    
    int res = 0;
    for(int i = 1; i <= n; i++)
    {
        memset(st, 0, sizeof st);
        if(find(i))
            res ++;
    }
        
    cout << n - res << endl;
    return 0;
}

4.2.2 最小路径点覆盖

这片树林里有 座房子, 条有向道路,组成了一张有向无环图。

树林里的树非常茂密,足以遮挡视线,但是沿着道路望去,却是视野开阔。

如果从房子 沿着路走下去能够到达 ,那么在 和 里的人是能够相互望见的。

现在 cl2 要在这 座房子里选择 座作为藏身点,同时 Vani 也专挑 cl2 作为藏身点的房子进去寻找,为了避免被 Vani 看见,cl2 要求这 个藏身点的任意两个之间都没有路径相连。

为了让 Vani 更难找到自己,cl2 想知道最多能选出多少个藏身点。

最小路径点覆盖 (minimum path vertex cover) 是一个有向图的问题,找出最少的路径,使得这些路径经过了所有的点。

最小路径点覆盖有两种类型:

  • 最小不相交路径覆盖 (minimum disjoint path vertex cover)

  • 最小可相交路径覆盖 (minimum intersecting path vertex cover)。

最小不相交路径覆盖要求每一条路径经过的顶点各不相同,而最小可相交路径覆盖允许每一条路径经过的顶点可以相同。

求解最小不相交路径覆盖数的一种方法是: 我们把每个点都拆成两个点,分为入点和出点,如果 u 到 v 有边,那么我们就让 u 的入点连向 v 的出点 , 最后跑一下最大流或者匈牙利算法求出最大匹配。 答案就是 点的数目 - 最大匹配数。

image-20230503191402514

故 最小路径点覆盖数等于顶点数减去最大匹配数。

而求最小可相交路径点覆盖问题时, 则需要先用 Floyd 求传递闭包, 对新图中再求一次最小路径覆盖即可。

4.2.3 最大独立集

给定一个 的棋盘,有一些格子禁止放棋子。

问棋盘上最多能放多少个不能互相攻击的骑士(国际象棋的“骑士”,类似于中国象棋的“马”,按照“日”字攻击,但没有中国象棋“别马腿”的规则)。

只需要求出最大匹配的数量, 就可以得出最大独立集数量。

该题若把每个格子能跳过去的格子之间建一条边, 那么无法攻击到的格子就是之间不存在边的格子, 满足这个条件的格子集合就是最大独立集。

本题的矩形排列的格子可以设行数加列数 为偶数的分为一边, 为奇数的分到另一边, 形成二分图, 而根据马走日的模式, 从一个点只能通过 +1+2 的方式到达另一个点, 因为 一个数+奇数 后会改变奇偶性, 故该图一定是二分图。

4.2.4 染色法判断二分图

bool dfs(int u, int c)
{
    color[u] = c;
    for(int i = h[u]; ~i; i = ne[i])
    {
        int j= e[i];
        if(!color[j])
        {
            if(!dfs(j,3 - c)) return false;
        }
        else if (color[j] == c) return false;
        
    }
    return true;
}

4.3 单源最短路

4.3.1 求比最短路多1长度的路径数

路径数量统计可以参考最短路计数 最短路径有几条, 定义一个 数组来统计数量。而这里的刚好多1的路径, 若存在, 其肯定是严格次短路径, 通过声明两维 , 一个代表最大值, 一个代表次大值即可。

若能更新最大值就直接更新, 若等于最大值则加上数量。若小于最大值但大于次大值, 则更新次大值, 若都不满足, 但和次大值相等, 则更新次大值的数量。 最后判断次大值是否刚好为最大值+1即可, 是则加上次大值的路径数。

typedef pair<int, int> PII;
typedef long long LL;
using i64 = long long;
// #define debug
const int N = 1e3 + 10, M = 2e4 + 10;
int h[N], e[M], w[M], ne[M], idx;
int S, F;
int dist[N][2], g[N][2];
bool st[N][2];
 
struct Node
{
    int id, type, dist;
    inline bool operator>(const Node &w) const
    {
        return dist > w.dist;
    }
};
 
int solve()
{
    mem(dist, 0x3f);
    mem(g, 0);
    mem(st, 0);
    priority_queue<Node, vector<Node>, greater<Node>> q;
    q.push({S, 0, 0});
    dist[S][0] = dist[S][1] = 0;
    g[S][0] = 1;
 
    while (q.size())
    {
        auto t = q.top();
        q.pop();
 
        int ver = t.id, type = t.type, d = t.dist;
        if (st[ver][type])
            continue;
        st[ver][type] = true;
 
        for (int i = h[ver]; ~i; i = ne[i])
        {
            int j = e[i];
            if (dist[j][0] > d + w[i])
            {
                dist[j][0] = d + w[i];
                g[j][0] = g[ver][type];
                q.push({j, 0, dist[j][0]});
            }
            else if (dist[j][0] == d + w[i])
                g[j][0] += g[ver][type];
            else if (dist[j][1] > d + w[i])
            {
                dist[j][1] = d + w[i];
                g[j][1] = g[ver][type];
                q.push({j, 1, dist[j][1]});
            }
            else if (dist[j][1] == d + w[i])
                g[j][1] += g[ver][type];
        }
    }
 
    int res = g[F][0];
    if (dist[F][0] + 1 == dist[F][1])
        res += g[F][1];
    return res;
}
 
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int T;
    cin >> T;
    while (T--)
    {
        mem(h, -1);
        int n, m;
        cin >> n >> m;
        while (m--)
        {
            int a, b, c;
            cin >> a >> b >> c;
            add(a, b, c);
        }
        cin >> S >> F;
        cout << solve() << endl;
    }
    return 0;
}

4.3.2 带钥匙最短路—多层图

如果还按照最短路中 走到第i个点的最短路径来考虑, 是没法知道当前是否有钥匙的。可以增加一维, 位置相同但此时拥有钥匙数量不同的情况算作不同的点, 最后求一遍最短路即可。 增加点后可以发现, 如果在当前点不动, 只捡钥匙的话, 不会耗时, 故该边权值为0, 其余走一格后的边权值为1, 既然是01图, 就可以用BFS双端队列来求解。

钥匙数就用二进制状态压缩。

typedef pair<int, int> PII;
// #define debug
typedef long long LL;
 
// using i64 = long long;
const int N = 110, M = 3e4;
int h[N], e[M], w[M], ne[M], idx;
int n, m, p;
int g[11][11];
int key[N];
int dist[N][1 << 10];
bool st[N][1 << 10];
 
void add(int a, int b, int c)
{
  e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
 
int bfs()
{
  deque<PII> q;
  mem(dist, 0x3f);
  q.push_front({g[1][1], 0});
  dist[1][0] = 0;
  while (q.size())
  {
    auto t = q.front();
    q.pop_front();
    int ver = t.F, keys = t.S;
    if (st[ver][keys])
      continue;
    st[ver][keys] = true;
    if (ver == g[n][m])
      return dist[ver][keys];
    if (key[ver])
    {
      int state = keys | key[ver];
      if (dist[ver][state] > dist[ver][keys])
      {
        dist[ver][state] = dist[ver][keys];
        q.push_front({ver, state});
      }
    }
 
    for (int i = h[ver]; ~i; i = ne[i])
    {
      int j = e[i];
      if (w[i] && !((keys >> (w[i] - 1)) & 1))
        continue;
      if (dist[j][keys] > dist[ver][keys] + 1)
      {
        dist[j][keys] = dist[ver][keys] + 1;
        q.push_back({j, keys});
      }
    }
  }
  return -1;
}
 
set<PII> Set;
void build()
{
  int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};
  for (int i = 1; i <= n; i++)
    for (int j = 1; j <= m; j++)
    {
      for (int k = 0; k < 4; k++)
      {
        int a = i + dx[k], b = j + dy[k];
        if (a < 1 || a > n || b < 1 || b > m)
          continue;
        if (Set.count({g[i][j], g[a][b]}))
          continue;
        add(g[i][j], g[a][b], 0);
      }
    }
}
 
int main()
{
  ios::sync_with_stdio(false);
  cin.tie(nullptr);
 
  while (cin >> n >> m >> p)
  {
    for (int i = 1, k = 1; i <= n; i++)
      for (int j = 1; j <= m; j++, k++)
        g[i][j] = k;
    mem(h, -1);
    int k;
    cin >> k;
    while (k--)
    {
      int x1, y1, x2, y2, c;
      cin >> x1 >> y1 >> x2 >> y2 >> c;
      int a = g[x1][y1], b = g[x2][y2];
      Set.insert({a, b});
      Set.insert({b, a});
      if (c)
        add(a, b, c), add(b, a, c);
    }
    build();
    int s;
    cin >> s;
    while (s--)
    {
      int a, b, c;
      cin >> a >> b >> c;
      a = g[a][b];
      key[a] |= 1 << (c - 1);
    }
    cout << bfs() << endl;
  }
  return 0;
}

4.3.3 多起点多终点最短路

通过建一个虚拟源点, 和各个起点连一条权值为0的边, 然后对所有点做最短路即可。

4.3.4 贸易差价最大最短路问题

赚最大钱的时候肯定是在最小价格时买入, 再之后走到的最大价格处卖出。

可以先从起点做最短路, 代表 中的最小价格, 再求一遍从终点开始的最长路, 代表 的最大价格。 最后再枚举所有点, 求出 的最大值即可。

const int N = 1e5 + 10, M = 2e6, INF = 0x3f3f3f3f;
int hs[N], ht[N], e[M], ne[M], idx;
int n,m;
int maxv[N], minv[N];
int w[N];
int q[N];
bool st[N];
 
void add(int h[], int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
 
void spfa(int h[], int dist[], bool flag)
{
    memset(st, 0, sizeof st);
    int hh = 0, tt = 1;
    if(flag) // 最小值
    {
        memset(dist, 0x3f, sizeof minv);
        q[0] = 1;
        dist[1] = w[1];
    }
    else
    {
        memset(dist, -0x3f, sizeof maxv);
        q[0] = n;
        dist[n] = w[n];
    }
    st[q[hh]] = true;
    while(hh != tt)
    {
        int t= q[hh++];
        if(hh == N) hh = 0;
        
        st[t] = false;
        
        for(int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            if(flag && dist[j] > min(dist[t], w[j]) || !flag && dist[j] < max(dist[t], w[j]))
            {
                if(flag) dist[j] = min(dist[t], w[j]);
                else dist[j] = max(dist[t], w[j]);
                if(!st[j])
                {
                    q[tt++] = j;
                    if(tt == N) tt = 0;
                    st[j] = true;
                }
            }
        }
    }
    
}
 
int main()
{
    
    memset(hs, -1, sizeof hs);
    memset(ht, -1, sizeof ht);
    cin >> n >> m;
    for(int i = 1; i <= n; i++) cin >> w[i];
    while(m--)
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(hs, a, b), add(ht, b, a);
        if(c == 2) add(hs, b, a), add(ht, a, b);
    }
    
    spfa(hs, minv, 1);
    spfa(ht, maxv, 0);
    
    int res = 0;
    for(int i = 1; i <= n; i++)
        res = max(res, maxv[i] - minv[i]);
        
    cout << res << endl;
    return 0;
}

4.3.5 第k大边最短路

给一个无向带权图, 求1~N的所有通路中, 第K+1大的边最小的路径。 输出的就是这K+1大的边的权值。

既然是最大值最小的问题, 可以尝试用二分结果来思考。 设当前选定K+1大的边权值为x, 我们需要确定是否在图中存在一条路径, 上面权值比x大的边有k个。 当x变小时, 1N的最短路显然比x大的会增加; 当x变大时, 1N的最短路显然比x大的会减少。

因此可以使用二分答案来求解, 至于1~N中最短路求权值比x大的边数量, 可以让大于x的权值设为1, 小于等于x的设为0, 这样就是01图, 可以使用双端队列BFS来以的复杂度解决掉。

bool check(int x)
{
    deque<int> q;
    memset(dist, 0x3f, sizeof dist);
    memset(st, 0, sizeof st);
    q.push_front(1);
    dist[1] = 0;
    
    while(q.size())
    {
        int t = q.front();
        q.pop_front();
        if(st[t]) continue;
        st[t] = true;
        for(int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i], v = w[i] > x;
            if(dist[j] > dist[t] + v)
            {
                dist[j] = dist[t] + v;
                if(!v) q.push_front(j);
                else q.push_back(j);
            }
        }
    }
    return dist[n] > k;
}
 
int main()
{
    /* 输入 */
    int l = -1, r = 1e6 + 1;
    while(l != r - 1)
    {
        int mid = l + r >> 1;
        if(check(mid)) l = mid;
        else r = mid;
    }
    if(r == (int)1e6 + 1) cout <<-1 <<endl;
    else cout << r << endl;
    
    return 0;
}

4.3.6 固定点经过最短路

从0点开始求得一条最短路径, 要求分别经过 a,b,c,d,e 点, 顺序不做要求。

以每一个车站为起点, 求一次全图最短路, 包括家在的地方0。 求完之后dfs遍历所有车站经过排列, 然后依次计算最短路求和即可。

void spfa(int start, long long dist[])
{
    memset(dist, 0x3f, N * 8);
    static int q[N];
    int hh = 0, tt = 1;
    q[0] = start;
    dist[start] = 0;
    while (hh != tt)
    {
        int t = q[hh++];
        if (hh == N)
            hh = 0;
        st[t] = false;
 
        for (int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                if (!st[j])
                {
                    q[tt++] = j;
                    if (tt == N)
                        tt = 0;
                    st[j] = true;
                }
            }
        }
    }
}
 
long long dfs(int u, int s, long long d)
{
    if (u == 6)
        return d;
 
    long long res = INF;
    for (int i = 1; i < 6; i++)
    {
        if (st[i])
            continue;
        int next = source[i];
        st[i] = true;
        res = min(res, dfs(u + 1, i, dist[s][next] + d));
        st[i] = false;
    }
 
    return res;
}
 
int main()
{
    memset(h, -1, sizeof h);
    cin >> n >> m;
 
    source[0] = 1;
    REP(i, 1, 6)
    cin >> source[i];
 
    while (m--)
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c);
        add(b, a, c);
    }
 
    for (int i = 0; i < 6; i++)
        spfa(source[i], dist[i]);
    // cout << dfs(1, 0, 0);
    int tmp[] = {0, 1, 2, 3, 4, 5}, cnt = 0, tot = 0, res = INF;
    while (1)
    {
        if (cnt == 120)
            break;
        cnt++;
        tot = dist[0][source[tmp[1]]];
        for (register int i = 1; i <= 4; i++)
            tot += dist[tmp[i]][source[tmp[i + 1]]];
        res = min(res, tot);
        next_permutation(tmp + 1, tmp + 6);
    }
    cout << res << endl;
 
    return 0;
}

4.3.7 点有效范围最短路

image-20230503213457002

求从虚拟源点到1号点的最短距离。 至于等级处理的话, 可以限制dijkstra搜索的范围, 只保证搜索等级区间长度为 的点。然后枚举所有这样且包含1号点的区间进行dijkstra即可。

int dijkstra(int l, int r)
{
    memset(dist, 0x3f, sizeof dist);
    memset(st, 0, sizeof st);
    dist[0] = 0;
 
    for(int i = 1; i <= n + 1; i++)
    {
        int t =-1;
        for(int j = 0; j <= n; j++)
            if(!st[j] && (t == -1 || dist[j] < dist[t]))
                t = j;
        
        st[t] = true;
        
        for(int j = 1; j <= n; j++)
            if(level[j] >= l && level[j] <= r)
                dist[j] = min(dist[j], dist[t] + w[t][j]);
    }
    
    return dist[1];
}
 
 
int main()
{
    cin >> m >> n;
    
    memset(w, 0x3f, sizeof w);
    for(int i = 0; i <= n; i++) w[i][i] = 0;
    
    for(int i = 1; i <= n; i++)
    {
        int price, cnt;
        cin >> price >> level[i] >> cnt;
        w[0][i] = min(w[0][i], price);
        while(cnt--)
        {
            int t, cost;
            cin >> t >> cost;
            w[t][i] = min(w[t][i], cost);
        }
    }
 
    int res = INF;
    
    for(int i = level[1] - m; i <= level[1]; i++) res = min(res, dijkstra(i, i + m));
    
    cout << res << endl;
    return 0;
}

4.3.8 有边数限制的最短路

const int N = 1e4 + 10, INF = 0x3f3f3f3f;
int dist[N], backup[N];
int n,m,k;
struct Edge
{
    int a,b,w;
}edge[N];
 
void bellmon_ford()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    for(int i = 0; i < k; i++)
    {
        memcpy(backup, dist, sizeof dist);
        for(int j = 0; j < m; j++)
        {
            int a = edge[j].a, b = edge[j].b, w = edge[j].w;
            dist[b] = min(dist[b], backup[a] + w);
        }
    }
    if(dist[n] > INF / 2) cout << "impossible";
    else cout << dist[n];
}
 
int main()
{
    cin >> n >> m >> k;
    for(int i = 0; i < m; i++)
    {
        int a, b , c;
        cin >> a >> b >> c;
        edge[i] = {a,b,c};
    }
    bellmon_ford();
    return 0;
}

4.4 多源最短路

4.4.1 求传递闭包

void floyd()
{
    memcpy(dist, g, sizeof dist);
    for (int k = 0; k < n; k++)
        for (int i = 0; i < n; i++)
            for (int j = 0; j < n; j++)
                dist[i][j] |= dist[i][k] && dist[k][j];
}

4.4.2 最小环问题

给定一张无向图,求图中一个至少包含 个点的环,环上的节点不重复,并且环上的边的长度之和最小。

输出最小环的方案,若最小环不唯一,输出任意一个均可。

const int INF = 0x3f3f3f3f, N = 1e2 + 50;
int g[N][N];
int dist[N][N];
int pos[N][N];
int path[N], cnt;
int n,m;
 
void getpath(int i, int j)
{
    if(pos[i][j] == 0) return;
    int k = pos[i][j];
    getpath(i,k);
    path[cnt++] = k;
    getpath(k,j);
}
 
 
int main()
{
    cin >> n >> m;
    memset(dist, 0x3f, sizeof dist);
    memset(g, 0x3f, sizeof g);
    for(int i = 1; i <= n; i ++) g[i][i] = 0, dist[i][i] = 0;
    while(m--)
    {
        int a, b, c;
        cin >> a >> b >> c;
        g[a][b] = min(g[a][b], c);
        g[b][a] = min(g[b][a], c);
        dist[a][b] = min(dist[a][b], c);
        dist[b][a] = min(dist[b][a], c);
    }
    
    long long res = INF;
    for(int k = 1; k <= n; k++)
    {
        for(int i = 1; i < k; i ++)
            for(int j = i + 1; j < k; j++)
                if((long long)g[i][k] + g[k][j] + dist[i][j] < res)
                {
                    res = g[i][k] + g[k][j] + dist[i][j];
                    cnt = 0;
                    path[cnt++] = i;
                    getpath(i,j);
                    path[cnt++] = j;
                    path[cnt++] = k;
                }
            
        for(int i = 1; i <= n; i++)
            for(int j = 1; j <= n; j++)
                if(dist[i][j] > dist[i][k] + dist[k][j])
                {
                    dist[i][j] = dist[i][k] + dist[k][j];
                    pos[i][j] = k;
                }
            
                
    }
    if(res == INF) cout << "No solution.\n";
    else {
        for(int i = 0; i < cnt; i++) cout << path[i] << " \n"[i == cnt-1];
    }
    return 0;
}

4.4.3 S到T恰好经过N条边的最短路

求从起点 到终点 恰好经过 条边(可以重复经过)的最短路。 可以初步定义状态为: 从 到 经过 条边的最短路径 仿照floyd的更新思路, 枚举另外一个点 :

假设这是其中一个正确答案image-20230503213848888 可以发现 和 之间并没有影响, 更新其中一个不会影响到另一个的取值。计算 可以由 和 转移过来, 而之后计算更多的边数时, 可以直接使用 代替两个小的状态。也就是说, 在计算结果 时, 用 和用 是等价的, 即满足结合律。

那么我们就可以联想到快速幂算法, 用 的复杂度逼近最终边长, 假如答案边长是 , 那我们只需要计算 边长时, 所有点组合的最短路径, 即 , 然后把边长相加, 就可以得到最终结果。这个过程是和快速幂非常相似的。

用 表示答案, 表示已经计算出来的算子, 根据快速幂的特性, 每次只需要用到当前长度的值, 下一次也只需要用当前长度的值的平方, 故不需要记录每个长度的情况, 删去 所在一维即可。即 。

const int N = 210;
unordered_map<int,int> ids;
int k,m,S,E,n = 1;
int res[N][N], g[N][N];
 
void mul(int c[][N], int a[][N], int b[][N])
{
    static int temp[N][N];
    memset(temp, 0x3f, sizeof temp);
    for(int k = 1; k < n; k++)
        for(int i = 1; i < n; i++)
            for(int j = 1; j < n; j++)
                temp[i][j] = min(temp[i][j], a[i][k] + b[k][j]);
    memcpy(c, temp, sizeof temp);
}
 
void qmi()
{
    memset(res, 0x3f, sizeof res);
    for(int i = 0; i <= n; i++) res[i][i] = 0;
    while(k)
    {
        if(k & 1) mul(res, res, g);
        mul(g, g, g);
        k >>= 1;
    }
}
 
int main()
{
    cin >> k >> m >> S >> E;
    ids[S] = n++;
    ids[E] = n++;
    S = ids[S];
    E = ids[E];
    
    memset(g, 0x3f, sizeof g);
    while(m--)
    {
        int a,b,c;
        cin >> c >> a >> b;
        if(!ids.count(a)) ids[a] = n++;
        if(!ids.count(b)) ids[b] = n++;
        a = ids[a], b = ids[b];
        g[a][b] = g[b][a] = min(g[a][b], c);
    }
    
    qmi();
    
    cout << res[S][E];
    
    return 0;
}

4.5 差分约束

差分约束 (1)求不等式组的可行解 源点需要满足条件:从源点出发, 一定可以走到所有边 步骤:

1. 先将每个不等式 $x_{i}\le x_{j}+ c_k$, 转化为一条从 $x_j$ 走到 $x_i$, 长度为 $c_k$ 的边
2. 找一个超级源点(从$x_{i}\le c$ 中得出), 使得该源点一定可以遍历到所有边
1. 从源点求一遍单源最短路
1. 如果有负环, 则无解
2. 如果没有负环, 则$dist[i]$就是原不等式组的一个可行解
(2)如何求最大值或者最小值, 这里的最值指的是每个变量的最值
结论:如果求得是最小值, 则应该求最长路; 如果求的是最大值, 应该求最短路。
求 $x_i$ 最大值:求所有从 $x_i$ 出发, 构成的不等式链:
$x_{i} \le x_{j}+ c_{1} \le x_{k} + c_{2} + c_{1} \le ... \le c_{1}+c_{2}+c_3+...$ 得到一个上界, 最终$x_i$ 的最大值就是所有上界的最小值
求 $x_i$ 最小值:求所有从 $x_i$ 出发, 构成的不等式链:
$x_{i} \ge x_{j}+ c_{1} \ge x_{k} + c_{2} + c_{1} \ge ... \ge c_{1}+c_{2}+c_3+...$ 得到一个下界, 最终$x_i$ 的最小值就是所有下界的最大值

4.5.1 前缀和类差分约束

N 头奶牛 ,编号为 。假设 号奶牛位于 ,则 。

有些奶牛是好基友,它们希望彼此之间的距离小于等于某个数。有些奶牛是情敌,它们希望彼此之间的距离大于等于某个数。

给出 对好基友的编号,以及它们希望彼此之间的距离小于等于多少;又给出 对情敌的编号,以及它们希望彼此之间的距离大于等于多少 。

请计算:如果满足上述所有条件, 号奶牛和 号奶牛之间的距离最大为多少。

设 为 的距离, 可以根据题目得出一些系列不等式:

  1. , 即
  2. , 即

再检验当前0点是否可以到达所有点, 因为条件一是从 , 故从0点不能到达所有点, 可以假设一个无限远地方有一个点, 连到所有点的权值为0, 也就是定义一个虚拟源点。实际上我们也不需要真的建边, 只需要在SPFA时把所有点加入队列中就行。

题目要求先判断负环, 那么需要把所有点加入其中。 然后判断是否可以无穷远, 图论中对应无穷远就是不存在从1到n的边, 我们只需要再做一次从1出发的SPFA即可, 若 则输出-2。

bool spfa(int size)
{
    memset(cnt, 0, sizeof cnt);
    memset(st, 0, sizeof st);
    memset(dist, 0x3f, sizeof dist);
    int hh = 0, tt = 0;
    for (int i = 1; i <= size; i++)
    {
        q[tt++] = i;
        dist[i] = 0;
        st[i] = true;
    }
    while (hh != tt)
    {
        int t = q[hh++];
        if (hh == N)
            hh = 0;
        st[t] = false;
 
        for (int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;
                if (cnt[j] >= n + 1)
                    return false;
                if (!st[j])
                {
                    st[j] = true;
                    q[tt++] = j;
                    if (tt == N)
                        tt = 0;
                }
            }
        }
    }
    return true;
}
 
int main()
{
    memset(h, -1, sizeof h);
    cin >> n >> m1 >> m2;
    for (int i = 1; i < n; i++)
        add(i + 1, i, 0);
 
    while (m1--)
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c);
    }
    while (m2--)
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(b, a, -c);
    }
 
    if (!spfa(n))
        cout << -1 << endl;
    else
    {
        spfa(1);
        if (dist[n] >= 0x3f3f3f3f / 2)
            cout << "-2" << endl;
        else
            cout << dist[n] << endl;
    }
 
    return 0;
}

若出现 时, 这类多了一个未确定的值, 我们可以直接进行枚举, 同时还需要加一个条件为 , 翻译过来就是这两个边:

4.6 拓扑排序

4.6.1 权值大于0的差分约束问题 O(n+m)

先做一遍拓扑排序, 然后按照拓扑排序的方案来DP求最长路即可。

const int N = 1e4 + 10, M = 4e4 + 10;
int n, m;
int q[N], money[N], res;
int h[N], ne[M], e[M], idx;
int din[N];
 
bool topsort()
{
    int hh = 0, tt = -1;
    for (int i = 1; i <= n; i++)
        if (!din[i])
            q[++tt] = i;
 
    while (hh <= tt)
    {
        int t = q[hh++];
 
        for (int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            if (--din[j] == 0)
                q[++tt] = j;
        }
    }
    if (tt < n - 1)
        return false;
    return true;
}
 
int main()
{
    memset(h, -1, sizeof h);
    memset(money, 0x3f, sizeof money);
    cin >> n >> m;
    while (m--)
    {
        int a, b;
        cin >> a >> b;
        add(b, a);
        din[a]++;
    }
    bool flag = topsort();
    if (!flag)
        cout << "Poor Xed";
    else
    {
        for (int i = 1; i <= n; i++)
            money[i] = 100;
        for (int i = 0; i < n; i++)
        {
            int j = q[i];
            for (int k = h[j]; ~k; k = ne[k])
                money[e[k]] = max(money[e[k]], money[j] + 1);
        }
        for (int i = 1; i <= n; i++)
            res += money[i];
        cout << res << endl;
    }
 
    return 0;
}

4.6.2 求一个点能到的点个数

给定一张 个点 条边的有向无环图,分别统计从每个点出发能够到达的点的数量。

朴素解法可以用DFS求每个点能走到的数量, 但时间复杂度最坏为 。

考虑一下DP 状态表示:f[i] i点能到的点的集合, 初始只有i自己 状态计算:f[i] = f[i] | f[j], j 为 i 的子节点, 取并集即可, 最后求一下集合中1的个数

若采用bool数组实现, 有n个点m条边, 时间复杂度最坏为 , 这里就用二进制来实现, 除了手写二进制, 也可以用STL库中的 bitset。

#include <bitset>
const int N = 3e4 + 10, M =2 * N;
int n,m;
int dist[N];
int q[N];
int din[N];
int h[N], ne[M], e[M], idx;
bitset<N> f[N];
 
 
void topsort()
{
    int hh= 0, tt = -1;
    for(int i = 1; i <= n; i++)
        if(!din[i]) q[++tt] = i;
    while(hh <= tt)
    {
        int t = q[hh++];
        for(int i = h[t]; ~i ; i = ne[i])
        {
            int j = e[i];
            if(--din[j] == 0) q[++tt] =j;
        }
    }
}
 
int main()
{
    memset(h, -1, sizeof h);
    cin >> n >> m;
    while(m--)
    {
        int a, b;
        cin >> a >> b;
        add(a,b);
        din[b]++;
    }
    
    topsort();
    
    for(int i = n - 1; i >= 0; i--)
    {
        int j = q[i];
        f[j][j] = 1;
        for(int k = h[j]; ~k; k = ne[k])
            f[j] |= f[e[k]];
    }
    
    for(int i = 1; i <= n; i++)
        cout << f[i].count() << endl;
    
    return 0;
}

4.6.3 全连接图拓扑序, 虚拟节点优化

image-20230503214848840

显然最低的级别数是2, 除非没有停靠站。停靠站的级别需要大于当前路径上其他无需停靠的站点 对于 1 3 5 6 这个车次, 每个停靠站的级别最小值 一定严格 大于 2 4 两个车站, 故可以像差分约数一样, a > b, 就从 b 连一条向 a 的边, 这里就这么连:image-20230503214902222

接近一个全连通图了, 遇到这种情况时就得考虑下会不会爆内存。 1000个车次, 假如都是从1到500车站, 那么就要要连 条边, 如果是邻接表会超内存, 要是用邻接矩阵, 在枚举的时候也会爆时间。

对于这种全连接类的, 有一种优化技巧: 在两边中间设立一个虚拟点, 将本来要分别连过去的边连到该点上

image-20230503214926090

这样就压缩了信息, 既可以保证从2点出发到达1356点的路径跟之前一样, 也降低了边数, 总共最多会有 条边, 少了很多。

建完图之后就拓扑排序, DP求最长路即可。 总复杂度为

const int N = 4100, M = 1e6;
int h[N], e[M], ne[M], idx, w[M];
int q[N];
int n, m;
int din[N], dist[N], res;
bool st[N];
 
void add(int a, int b, int c)
{
    e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
    din[b]++;
}
 
void topsort()
{
    int hh = 0, tt = -1;
    for (int i = 1; i <= n + m; i++)
        if (!din[i])
            q[++tt] = i;
    while (hh <= tt)
    {
        int t = q[hh++];
 
        for (int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            if (--din[j] == 0)
                q[++tt] = j;
        }
    }
}
 
int main()
{
    cin >> n >> m;
    memset(h, -1, sizeof h);
    memset(dist, 0, sizeof dist);
    for (int i = 1; i <= m; i++)
    {
        memset(st, 0, sizeof st);
        int cnt;
        cin >> cnt;
        int start = n, end = 1;
        while (cnt--)
        {
            int t;
            cin >> t;
            st[t] = true;
            start = min(t, start);
            end = max(t, end);
        }
 
        int ver = n + i; // 虚拟节点
        for (int j = start; j <= end; j++)
        {
            if (!st[j])
                add(j, ver, 0);
            else
                add(ver, j, 1);
        }
    }
 
    topsort();
 
    for (int i = 1; i <= n; i++)
        dist[i] = 1;
    for (int i = 0; i < n + m; i++)
    {
        int j = q[i];
        for (int k = h[j]; ~k; k = ne[k])
            dist[e[k]] = max(dist[e[k]], dist[j] + w[k]);
    }
 
    for (int i = 1; i <= n; i++)
        res = max(res, dist[i]);
    cout << res << endl;
    return 0;
}

4.7 最小生成树

在一个无向图内求出将所有点连通起来的最小权值和, 也就是最小生成树。

通常有两个算法:

  • Prim算法

    1. 建完图后进行类似dijkstra的操作
    2. 循环n-1次, 每次使用当前权值最小的点, 更新它能到的所有点
  • Kruskal算法

    1. 对所有边排序
    2. 创建一个并查集, 然后从小到大遍历所有边
    3. 若当前边的两个点不在一个集合内, 就将其合并, 并加上该边的权值

其中Prim的复杂度是 , Kruscal的复杂度是 。 一般稀疏图用Kruscal, 稠密图用Prim, 不过大部分情况下用Kruscal都可以。

int prim()
{
    memset(dist, 0x3f, sizeof dist);
    int res = 0;
    dist[1] = 0;
    for (int i = 1; i <= n; i++)
    {
        int t = -1;
        for (int j = 1; j <= n; j++)
            if (!st[j] && (t == -1 || dist[j] < dist[t]))
                t = j;
        st[t] = true;
        res += dist[t];
 
        for (int j = 1; j <= n; j++)
            if (g[t][j])
                dist[j] = min(dist[j], g[t][j]);
    }
    return res;
}

4.7.1 无向图删边—边数最小权值最小

最小生成树满足条件。连通性满足, 边数最小也是可以满足, 最多连n-1条边。

至于最大边权值最小, 算法求解时会把当前边之前, 也就是比该边权值更小的边都已经纳入图中, 如果需要在当前a,b两点连一条边, 那么只有当前的边是最合适的, 因为后边的权值都比当前权值大。

故这个最大值就是最后一条连接的边, 我们每次更新的时候记录下来当前边的权值即可, 这样结束的时候记录的就是最后一个。

4.7.1 无向图选边—已经选了k条边

一个无向图, 求保证连通性的情况下, 选择一些边使其权值和最小。同时, 有一些边必须选。

Kruscal算法有个特性就是无论执行到哪个阶段, 已经执行过的部分也是正确的。故可以先手动把必须选的边加入到并查集中, 剩下的再进行一次Kruscal。

const int N = 2e3 + 10, M = 1e4 + 10;
int f[N];
int n, m, cnt;
struct Node
{
    int a, b, c;
    bool operator<(const Node &w) const
    {
        return c < w.c;
    }
} e[M];
 
int find(int x) { return f[x] == x ? f[x] : f[x] = find(f[x]); }
 
int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        f[i] = i;
    int res = 0;
    while (m--)
    {
        int a, b, c, p;
        cin >> p >> a >> b >> c;
        if (p == 1)
        {
            f[find(b)] = find(a);
            res += c;
        }
        else
        {
            e[cnt++] = {a, b, c};
        }
    }
    sort(e, e + cnt);
    for (int i = 0; i < cnt; i++)
    {
        int a = find(e[i].a), b = find(e[i].b), c = e[i].c;
        if (a != b)
        {
            f[b] = a;
            res += c;
        }
    }
    cout << res << endl;
    return 0;
}

4.7.2 多起点最小生成树

选择 k 个点建立发电站, 费用为 v。或者让某个点链接到附近已经有电力供应的点, 费用为 p。求让所有点连通且花费最小的方案。

任选一个点A建立发电站, 然后让所有矿井与该点相连, 可以看做是从A点为起点的最小生成树。而最优结果需要考虑其他矿井为起点的情况。

可以枚举每一个矿井为起点的情况么?显然不行, 最优结果可能不止放一个发电站, 若状态压缩枚举所有放置情况, 状态数又太多。

回忆一下单源最短路的问题, 有个应对多起点的技巧, 建立虚拟源点。这里就创建一个虚拟源点, 和其他所有点链接, 权值为建发电站的费用。接着从该源点求最小生成树即可。

const int N = 3e2 + 10;
int n, m;
int g[N][N];
int dist[N];
bool st[N];
 
int prim()
{
    memset(dist, 0x3f, sizeof dist);
    dist[0] = 0;
    int res = 0;
    for (int i = 0; i < n + 1; i++)
    {
        int t = -1;
        for (int j = 0; j <= n; j++)
            if (!st[j] && (t == -1 || dist[j] < dist[t]))
                t = j;
 
        st[t] = true;
        res += dist[t];
 
        for (int j = 0; j <= n; j++)
            dist[j] = min(dist[j], g[t][j]);
    }
 
    return res;
}
 
int main()
{
    cin >> n;
    for (int i = 1; i <= n; i++)
    {
        int t;
        cin >> t;
        g[0][i] = t;
    }
 
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            cin >> g[i][j];
 
    cout << prim();
 
    return 0;
}

4.7.3 连通块划分

给一个无向图, 为了让所有点连通, 给你k个卫星站, 任意两个卫星站之间可以直接通讯, 距离为0, 但卫星站的数量不一定够用, 有一部分之间是无法通过卫星相连, 故又让你选一种型号的无线电收发机, 给无法通过卫星相连的村庄。无线电收发机不同型号有不同的范围d, 输出让整个图连通所能用的最小的d。

给了我们两个有关条件, 卫星站和无线电, 假设每个村庄都有无线电, 村庄之间距离小于等于d的就会自行连接到一起, 而卫星站的作用就是连接这些分散的连通块。即有多少个连通块就需要用多少个卫星。

显然可以得出一个单调性, d值越大, 连通块数量越小, 卫星数量越小。由此可以用二分d值, dfs求连通块数量, 可以在 的复杂度下解决。

也可以用Kruscal算法, 在枚举最小边时连通块数量最大, 每连接不在一通块内部的一对点, 就会让总连通块数量减一, 当减到 时就停下来, 输出当前的权值即可。

#include <iostream>
#include <cstring>
#include <algorithm>
#include <string>
#include <cmath>
#include <iomanip>
using namespace std;
pair<double, double> g[550];
const int N = 250100;
struct Edge
{
    int a, b;
    double w;
    bool operator<(const Edge &W) const
    {
        return w < W.w;
    }
} e[N];
int n, m, cnt;
int f[550];
 
double get_dist(int i, int j)
{
    double x = g[i].first - g[j].first;
    double y = g[i].second - g[j].second;
    return sqrt(x * x + y * y);
}
 
int find(int x) { return f[x] == x ? f[x] : find(f[x]); }
 
int main()
{
    cout << fixed << setprecision(2);
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        cin >> g[i].first >> g[i].second;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            e[cnt++] = {i, j, get_dist(i, j)};
    sort(e, e + cnt);
    int res = n;
    for (int i = 1; i <= n; i++)
        f[i] = i;
    for (int i = 0; i < cnt; i++)
    {
        int a = find(e[i].a), b = find(e[i].b);
        double w = e[i].w;
        if (a != b)
        {
            res--;
            f[b] = a;
            if (res == m)
            {
                cout << w << endl;
                break;
            }
        }
    }
    return 0;
}

4.7.4 增加边使得树扩充为完全图, 且唯一最小生成树仍是原来的

给定一棵 个节点的树,要求增加若干条边,把这棵树扩充为完全图,并满足图的唯一最小生成树仍然是这棵树。

求增加的边的权值总和最小是多少。

注意: 树中的所有边权均为整数,且新加的所有边权也必须为整数。

首先考虑怎么通过这n-1条边构成完全图, 且还要利于保留最小生成树, 正如设计密码一题, 其用正解来更新其他状态的思想在这里也体现了出来。

采用kruscal算法来对n-1条边求最小生成树, 再加完一条边A后, 将两个即将相连的集合加一些边构成完全图, 要加的边权值必须满足刚好大于原本的边A。若相等则会出现不唯一的最小生成树, 若小于则原本的最小生成树失效。

const int N = 6e3 + 10;
int f[N], Size[N];
struct Edge {
    int a, b, c;
    bool operator < (const Edge &W) const{
        return c < W.c;
    }
}e[N];
int n;
 
int find(int x) {return f[x] == x ? f[x] : f[x] = find(f[x]);}
 
int main()
{
    int t ;
    cin >> t;
    while(t--)
    {
        cin >> n;
        for(int i = 1; i <= n; i++) f[i] = i, Size[i] = 1;
        for(int i = 0; i < n - 1; i++)
        {
            int a, b, c;
            cin >> a >> b >> c;
            e[i] = {a,b,c};
        }
        sort(e, e + n - 1);
        int res = 0;
        for(int i = 0; i < n-1; i++)
        {
            int a = find(e[i].a), b = find(e[i].b), c = e[i].c;
            if(a != b)
            {
                res += (Size[a] * Size[b] - 1) * (c + 1);
                f[b] = a;
                Size[a] += Size[b];
            }
        }
        cout  << res << endl;
    }
    
    return 0;
}

4.7.5 非严格次小生成树

  1. 求最小生成树, 对于每条边标记是树边还是非树边, 并建立最小生成树图。
  2. DFS预处理任意两点间的边权最大值dist
  3. 依次枚举所有非树边, 求, 满足 。

故总时间复杂度为 。若用LCA优化可以到 。

const int N = 2e3 + 10, M = 2e4 + 10;
typedef long long LL;
int f[N];
int n, m;
struct Edge
{
    int a, b, c;
    bool flag;
    bool operator<(const Edge &W) const
    {
        return c < W.c;
    }
} e[M];
int h[N], ne[N * N], E[N * N], w[N * N], idx;
int dist[N][N];
 
int find(int x) { return f[x] == x ? f[x] : f[x] = find(f[x]); }
void add(int a, int b, int c) { E[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++; }
 
void dfs(int u, int fa, int maxd, int d[])
{
    d[u] = maxd;
    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = E[i];
        if (j == fa)
            continue;
 
        dfs(j, u, max(maxd, w[i]), d);
    }
}
 
int main()
{
    cin >> n >> m;
    memset(h, -1, sizeof h);
    for (int i = 1; i <= n; i++)
        f[i] = i;
    for (int i = 0; i < m; i++)
    {
        int a, b, c;
        cin >> a >> b >> c;
        e[i] = {a, b, c};
    }
    LL sum = 0;
    sort(e, e + m);
    for (int i = 0; i < m; i++)
    {
        int a = e[i].a, b = e[i].b, c = e[i].c;
        int pa = find(a), pb = find(b);
        if (pa != pb)
        {
            f[pb] = pa;
            e[i].flag = true;
            add(a, b, c), add(b, a, c);
            sum += c;
        }
    }
    for (int i = 1; i <= n; i++)
        dfs(i, 0, 0, dist[i]);
 
    LL res = 1e18;
    for (int i = 0; i < m; i++)
    {
        if (e[i].flag)
            continue;
        int a = e[i].a, b = e[i].b, c = e[i].c;
        if (c > dist[a][b])
            res = min(sum + c - dist[a][b], res);
    }
 
    cout << res << endl;
    return 0;
}

4.7.6 严格次小生成树

请查阅 LCA 模块

4.8 最近公共祖先 LCA

4.8.1 求 a,b 的最近公共祖先

时间复杂度为: 预处理采用BFS 查询

const int N = 4e4 + 10, M = 2 * N;
int h[N], ne[M], e[M], idx;
int depth[N];
int f[N][16];
int n, m;
int q[N];
 
void add(int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx++; }
 
void bfs(int root)
{
    memset(depth, 0x3f, sizeof depth);
    int hh = 0, tt = 0;
    q[0] = root;
    depth[root] = 1, depth[0] = 0;
    while (hh <= tt)
    {
        int t = q[hh++];
        for (int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            if (depth[j] > depth[t] + 1)
            {
                depth[j] = depth[t] + 1;
                q[++tt] = j;
                f[j][0] = t;
                for (int k = 1; k < 16; k++)
                    f[j][k] = f[f[j][k - 1]][k - 1];
            }
        }
    }
}
 
int lca(int a, int b)
{
    if (depth[a] < depth[b])
        swap(a, b);
    for (int k = 15; k >= 0; k--)
        if (depth[f[a][k]] >= depth[b])
            a = f[a][k];
    if (a == b)
        return a;
    for (int k = 15; k >= 0; k--)
        if (f[a][k] != f[b][k])
            a = f[a][k], b = f[b][k];
    return f[a][0];
}
 
int main()
{
    int root = -1;
    memset(h, -1, sizeof h);
    cin >> n;
    while (n--)
    {
        int a, b;
        cin >> a >> b;
        if (b == -1)
            root = a;
        else
            add(a, b), add(b, a);
    }
    cin >> m;
    bfs(root);
    while (m--)
    {
        int a, b;
        cin >> a >> b;
        int p = lca(a, b);
        if (p == a)
            cout << 1 << endl;
        else if (p == b)
            cout << 2 << endl;
        else
            cout << 0 << endl;
    }
    return 0;
}

4.8.2 树上多源最短路&&离线LCA

给出 个点的一棵树,多次询问两点之间的最短距离。

预处理每个点到根节点的距离 , 对于 两点间的距离, 就是 , 其中 是 的最小公共祖先。

若采用倍增LCA来求, 时间复杂度为 , 可以通过。不过这里介绍下更快的做法:tarjan离线求LCA。离线是指把所有查询存下来后一并处理且输出, 在线则是读入一个查询处理一个查询。

typedef pair<int,int> PII;
const int N = 10010, M = N * 2;
int h[N], w[M], e[M], ne[M], idx;
int dist[N];
int res[M];
int f[N];
vector<PII> query[N];
int n,m;
int st[N];
 
void add(int a, int b, int c) {e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;}
 
void dfs(int u, int fa)
{
    for(int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if(j == fa) continue;
        dist[j] = dist[u] + w[i];
        dfs(j,u);
    }
}
 
int find(int x) {
    if(f[x] != x) f[x] = find(f[x]);
    return f[x];
}
 
void tarjan(int u)
{
    st[u] = 1;
    for(int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if(!st[j])
        {
            tarjan(j);
            f[j] = u;
        }
    }
 
    for(auto item : query[u])
    {
        int a = item.first, id = item.second;
        if(st[a] == 2)
        {
            int anc = find(a);
            res[id] = dist[u] + dist[a] - 2 * dist[anc];
            //cout << res[id] << endl;
        }
    }
    
    st[u] = 2;
}
 
int main()
{
    memset(h, -1, sizeof h);
    cin >> n >> m;
    for(int i = 1; i < n; i++)
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(a,b,c), add(b,a,c);
    }
    for(int i = 0; i < m; i++)
    {
        int a, b;
        cin >> a >> b;
        if(a != b)
        {
            query[a].push_back({b,i});
            query[b].push_back({a,i});
        }
    }
    for(int i = 1; i <= n; i++) f[i] = i;
    dfs(1,-1);
    tarjan(1);
    
    for(int i = 0; i < m; i++) cout << res[i] << "\n";
    
    return 0;
}

4.8.3 严格次小生成树

const int N = 1e5 +10, M = 3e5 + 10, INF = 0x3f3f3f3f;
typedef long long LL;
int h[N], e[M], ne[M], w[M], idx;
int p[N];
int f[N][17], d1[N][17], d2[N][17];
int depth[N], q[N];
struct Edge
{
    int a, b, c;
    bool used;
    bool operator <(const Edge &W) const {
        return c < W.c;
    }
}edge[M];
int n,m;
 
void add(int a, int b, int c) {e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;}
 
int find(int x) {return p[x] == x? p[x] : p[x] = find(p[x]);}
 
LL kruscal()
{
    memset(h, -1, sizeof h);
    idx = 0;
    LL res = 0;
    for(int i = 1; i <= n; i++) p[i] = i;
    sort(edge, edge + m);
    for(int i = 0; i < m; i++)
    {
        int a = edge[i].a, b = edge[i].b, c = edge[i].c;
        int pa = find(a), pb = find(b);
        if(pa != pb)
        {
            p[pb] = pa;
            res += c;
            add(a, b, c), add(b,a,c);
            edge[i].used = true;
        }
    }
    
    return res;
}
 
 
void bfs()
{
    memset(depth, 0x3f, sizeof depth);
    int hh = 0, tt = 0;
    depth[0] = 0, depth[1] = 1;
    q[0] = 1;
    while(hh <= tt)
    {
        int t = q[hh++];
        for(int i = h[t]; ~i ; i = ne[i])
        {
            int j = e[i];
            if(depth[j] > depth[t] + 1)
            {
                depth[j] = depth[t] + 1;
                q[++tt] = j;
                f[j][0] = t;
                d1[j][0] = w[i], d2[j][0] = -INF;
                for(int k = 1; k < 17; k++)
                {
                    int anc = f[j][k-1];
                    f[j][k] = f[anc][k-1];
                    int distance[4] = {d1[j][k-1], d2[j][k-1], d1[anc][k-1], d2[anc][k-1]};
                    d1[j][k] = d2[j][k] = -INF;
                    for(int u = 0; u < 4; u++)
                    {
                        int &d = distance[u];
                        if(d > d1[j][k]) d2[j][k] = d1[j][k], d1[j][k] = d;
                        else if(d != d1[j][k] && d > d2[j][k]) d2[j][k] = d;
                    }
                }
            }
        }
    }
}
 
 
 
int lca(int a, int b, int c)
{
    static int distance[N*2];
    int cnt= 0;
    if(depth[a] < depth[b]) swap(a,b);
    for(int k = 16; k >= 0; k--)
        if(depth[f[a][k]] >= depth[b])
        {
            distance[cnt++] = d1[a][k];
            distance[cnt++] = d2[a][k];
            a = f[a][k];
        }
    if(a != b)
    {
        for(int k = 16; k >= 0; k--)
            if(f[a][k] != f[b][k])
            {
                distance[cnt++] = d1[a][k];
                distance[cnt++] = d2[a][k];
                distance[cnt++] = d1[b][k];
                distance[cnt++] = d2[b][k];
                a = f[a][k];
                b = f[b][k];
            }
        distance[cnt++] = d1[a][0];
        distance[cnt++] = d1[b][0];
    }
    int dist1 = -INF, dist2 = -INF;
    for(int i = 0; i < cnt; i ++)
    {
        int &d = distance[i];
        if(d > dist1) dist2 = dist1, dist1 = d;
        else if(d != dist1 && d > dist2) dist2 = d;
    }
    
    if(c > dist1) return c - dist1;
    else if(c > dist2) return c - dist2;
    return INF;
}
 
int main()
{
    cin >> n >> m;
    for(int i = 0; i < m; i++)
    {
        int a, b, c;
        cin >> a >> b >> c;
        edge[i] = {a,b,c};
    }
    
    // 求最小生成树
    LL sum = kruscal();
 
    // 预处理 f, d1, d2
    bfs();
    
    // 枚举非树边
    LL res = 1e18;
    for(int i = 0; i < m; i++)
        if(!edge[i].used)
        {
            int a = edge[i].a, b = edge[i].b, c = edge[i].c;
            res = min(res, sum + lca(a,b, c));
        }
    cout << res << endl;
    
    return 0;
}

4.8.4 删去主要边+次要边使图不连通

给 个节点和两类边, 第一类共 条, 且任意两个节点之间都存在一条路径, 也就构成了一个树。第二类共 条, 是附加边, 也就是非树边。 让求把该图通过切断一条树边和一条非树边, 使其不连通。输出所有方案数。

差分, 在 上 对应到树中就是:

  1. 在 节点
  2. 在 节点
  3. 在 节点 , 其中 为 的最小公共祖先

这样首先保证了在 子树之外的点不会受影响, 因为 和 抵消。而每个点的权值代表其连接父节点向上走的边的权值, 等于其子树节点权值之和。 这样就可以用 的复杂度统计差分, 最后再用 的复杂度 DFS 求子树之和。同时求解答案。

const int N = 1e5 + 10, M = 2 * N;
int h[N], e[M], ne[M], idx;
int cnt[M];
int depth[N], f[N][17];
int n,m;
int q[N];
int ans;
 
void add(int a, int b) {e[idx] = b, ne[idx] = h[a], h[a] = idx++;}
 
void bfs()
{
    memset(depth, 0x3f, sizeof depth);
    depth[0] = 0, depth[1] = 1;
    int hh = 0, tt = 0;
    q[0] = 1;
    while(hh <= tt)
    {
        int t = q[hh++];
        for(int i = h[t]; ~i ; i = ne[i])
        {
            int j = e[i];
            if(depth[j] > depth[t] + 1)
            {
                q[++tt] = j;
                depth[j] = depth[t] + 1;
                f[j][0] = t;
                for(int k = 1; k <= 16; k++)
                    f[j][k] = f[f[j][k-1]][k-1];
            }
        }
    }
}
 
int lca(int a, int b)
{
    if(depth[a] < depth[b]) swap(a,b);
    for(int k = 16; k >= 0; k--)
        if(depth[f[a][k]] >= depth[b])
            a = f[a][k];
    if(a == b) return a;
    for(int k = 16; k >= 0; k--)
        if(f[a][k] != f[b][k])
            a = f[a][k], b = f[b][k];
    return f[a][0];
}
 
int dfs(int u, int fa)
{
    int res = cnt[u];
    for(int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if(j == fa) continue;
        int s = dfs(j, u);
        if(s == 0) ans += m;
        else if(s == 1) ans++;
        res += s;
    }
    return res;
}
 
int main()
{
    memset(h , -1, sizeof h);
    cin >> n >> m;
    for(int i = 1; i < n; i ++)
    {
        int a, b;
        cin >> a >> b;
        add(a,b), add(b,a);
    }
    bfs();
    
    for(int i = 0; i < m; i++)
    {
        int a, b;
        cin >> a >> b;
        int p = lca(a,b);
        cnt[a]++, cnt[b]++, cnt[p] -= 2;
    }
    
    dfs(1,-1);
    cout << ans << endl;
    
    return 0;
}

4.9 欧拉回路和欧拉路径

4.9.1 欧拉回路

有向图:存在欧拉回路的充分必要条件:所有点的出度均等于入度。

无向图:存在欧拉回路的充分必要条件:度数为奇数的点只能有0个。

4.9.2 欧拉路径

有向图:存在欧拉路径的充分必要条件:要么所有点的出度均等于入度;要么除了两个点之外,其余所有点的出度等于入度,剩余的两个点:一个满足出度比入度多1(起点),另一个满足入度比出度多1(终点).

无向图:存在欧拉路径的充分必要条件:度数为奇数的点只能有0或2个。

4.9.3 输出欧拉回路方案

枚举时直接让 , 边缩边搜, 就是 复杂度了。 如果碰到之前搜过的边 则直接 跳过即可。若没搜过则加入到结果序列中, 注意这里是添加编号, 且若为无向边则只添加一条边。 有向边的编号就是 , 而无向图的编号则是 , 题目中还要求如果是反向边需要输出负数, 因为正反两条边添加是一起的, 第一条, 第二条 故正向边都会是奇数, 而反向边都是偶数。

1 是无向图, 2 是有向图。

const int N = 1e5 + 10, M = 4e5 + 10;
int h[N], ne[M], e[M], idx;
int n, m;
int type;
int din[N], dout[N];
bool used[M];
int cnt, res[M];
 
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a];
    h[a] = idx++;
}
 
void dfs(int u)
{
    for (int &i = h[u]; ~i;)
    {
        if (used[i])
        {
            i = ne[i];
            continue;
        }
        used[i] = true;
 
        int t;
        if (type == 1)
        {
            used[i ^ 1] = true;
            t = i / 2 + 1;
            if (i & 1)
                t = -t;
        }
        else
            t = i + 1;
        int j = e[i];
        i = ne[i];
 
        dfs(j);
        res[++cnt] = t;
    }
}
 
int main()
{
    memset(h, -1, sizeof h);
    cin >> type;
    cin >> n >> m;
    for (int i = 0; i < m; i++)
    {
        int a, b;
        cin >> a >> b;
        add(a, b);
        if (type == 1)
            add(b, a);
        din[b]++;
        dout[a]++;
    }
 
    if (type == 1)
    {
        for (int i = 1; i <= n; i++)
            if ((din[i] + dout[i]) & 1)
            {
                cout << "NO\n";
                return 0;
            }
    }
    else if (type == 2)
    {
        for (int i = 1; i <= n; i++)
            if (din[i] != dout[i])
            {
                cout << "NO\n";
                return 0;
            }
    }
    for (int i = 1; i <= n; i++)
        if (h[i] != -1)
        {
            dfs(i);
            break;
        }
    if (cnt < m)
    {
        cout << "NO\n";
        return 0;
    }
    cout << "YES\n";
    for (int i = cnt; i; i--)
        cout << res[i] << " ";
 
    return 0;
}

4.9.4 输出欧拉路径方案

根据欧拉回路搜索过程, 从一个点出发也一定会再回来, 且答案是逆序存储, 故假如从 点出发, 下一个点是 , 那么对应到答案就是 , 显然, 我们需要选择能走的最小的 。 若边数太多需要用邻接表时, 得排个序, 而这里点数比较小, 可以直接从小到大枚举。

代码流程:

  1. 读入边并统计出入度
  2. 找到起点start, 先定义为度数为0的点(0个时)
  3. 然后遍历所有点判断是否存在度数为奇数的点, 若有则为起点(2个时)
  4. 从起点进行DFS搜索
  5. 输出答案
const int N = 550, M = 1026;
int g[N][N];
int d[N];
int n;
int res[M];
int cnt;
 
void dfs(int u)
{
    for(int i = 1; i <= 500; i++)
    {
        if(g[u][i])
        {
            g[u][i]--, g[i][u]--;
            dfs(i);
        }
    }
    res[++cnt] = u;
}
 
int main()
{
    cin >> n;
    for(int i = 0; i < n;i ++)
    {
        int a,b;
        cin >> a >> b;
        g[a][b]++, g[b][a]++;
        d[a]++, d[b]++;
    }
    int start = 1;
    while(!d[start]) start++;
    for(int i = 1; i <= 500; i++)
        if(d[i] & 1)
        {
            start = i;
            break;
        }
    dfs(start);
    for(int i = cnt; i; i--) cout << res[i] << "\n";
    return 0;
}

4.9.5 应用:字符串头尾连接

有 个盘子,每个盘子上写着一个仅由小写字母组成的英文单词。

你需要给这些盘子安排一个合适的顺序,使得相邻两个盘子中,前一个盘子上单词的末字母等于后一个盘子上单词的首字母。

请你编写一个程序,判断是否能达到这一要求。

边读入边记录出入度, 用 start 和 end 记录当前符合起点或终点的点数量。只有其他点都入度都等于出度或者start和end点数量是0个或者1个。

判断连通可以用并查集, 刚开始建边时就合并, 最后枚举所有出现的字符, 判断是否跟之前的在一个集合里, 若不在说明不连通。

记得用st数组标记目前出现过的字符。

const int N = 1e5 + 10, M = 2 * N;
int din[29], dout[29], f[N];
int n;
bool st[N];
 
int find(int x) { return f[x] == x ? f[x] : f[x] = find(f[x]); }
 
int main()
{
    int T;
    cin >> T;
    while (T--)
    {
        memset(din, 0, sizeof din);
        memset(dout, 0, sizeof dout);
        memset(st, 0, sizeof st);
        for (int i = 0; i < 26; i++)
            f[i] = i;
        cin >> n;
        for (int i = 0; i < n; i++)
        {
            string s;
            cin >> s;
            int b = s[s.size() - 1] - 'a', a = s[0] - 'a';
            st[a] = st[b] = true;
            f[find(a)] = find(b);
            din[b]++;
            dout[a]++;
        }
        bool flag = true;
        int start = 0, end = 0;
        for (int i = 0; i < 26; i++)
            if (din[i] != dout[i] && st[i])
            {
                if (din[i] == dout[i] + 1)
                    end++;
                else if (din[i] + 1 == dout[i])
                    start++;
                else
                {
                    flag = false;
                    break;
                }
            }
        if (flag && !((!start && !end) || (start == 1 && end == 1)))
            flag = false;
 
        int p = -1;
        for (int i = 0; i < 26; i++)
        {
            if (!st[i])
                continue;
            if (p == -1)
                p = find(i);
            else if (p != find(i))
            {
                flag = false;
                break;
            }
        }
        if (flag)
            puts("Ordering is possible.");
        else
            puts("The door cannot be opened.");
    }
 
    return 0;
}

4.10 SPFA判断负环

4.10.1 01分数规划

求有向图的一个环, 使“环上各点的权值之和”除以“环上各边的权值之和”最大。输出最大值。即 最大。

这种类似的问题就是01分数规划问题, 有模板解法。其结果是存在上下界的, 最小因为权值都是正整数, 故为(开区间), 最大值则是分母为, 分子最大 , 故最大值为。

而其两段性是很显然的, 设任取中间一个值 , 判断能否找到一个环的值大于, 如果不能, 说明图中最大值小于, 继续在 中搜索。若搜得到, 就说明图中最大值存在与 之间, 继续缩小范围。最终会找到满足条件的最大值。

判断是否存在一个环满足 先做一个数学上的变换: 把求和符号提出可得等价式子: 当我们把这求和里面那块看做从其他点到点的边, 那么这个式子的含义就是, 存在一个环, 它的边权和大于0。既然小于0的叫做负环, 那大于0就叫做正环。

求正环的操作跟负环类似, 只需要把求最小值换成求最大值即可。

求正负环还有个优化操作, 当寻找到一定次数之后可以提前结束以避免超时, 即定义一个变量 记录当前搜索次数, 超过一般 时就提前返回。

也可以把队列改成栈, 这样搜索模式为深度优先, 会更多的去更新刚入栈的点, 让数组累加的更快。

const int N = 1e3 + 10, M = 1e5 + 10;
int n,m;
int h[N], e[M], w[M], ne[M], idx;
int f[N];
int q[N], cnt[N];
double dist[N];
bool st[N];
 
void add(int a, int b, int c) {e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;}
 
bool check(double mid)
{
    memset(st, 0, sizeof st);
    memset(cnt, 0, sizeof cnt);
    int hh = 0, tt = 0;
    for(int i = 1; i <= n; i++)
        q[tt++] = i, st[i] = true;
    
    while(hh != tt)
    {
        int t = q[--tt];
        
        st[t] = false;
        for(int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            if(dist[j] < dist[t] - w[i]*mid + f[j])
            {
                dist[j] = dist[t] - w[i]*mid + f[j];
                cnt[j] = cnt[t] + 1;
                if(cnt[j] >= n) return true;
                if(!st[j])
                    q[tt++] = j, st[j] = true;
            }
        }
    }
    return false;
}
 
int main()
{
    memset(h, -1, sizeof h);
    cin >> n >> m;
    for(int i = 1; i <= n; i++) cin >> f[i];
    while(m--)
    {
        int a, b , c;
        cin >> a >> b >> c;
        add(a,b,c);
    }
    
    double l = 1, r = 1000;
    while(r - l > 1e-4)
    {
        double mid = (l + r) / 2.0;
        if(check(mid)) l = mid;
        else r = mid;
    }
    printf("%.2lf", l);
    return 0;
}

4.10.2 字符串连接求最长环, 字母优化

给几个字符串, 其中若A串末尾两个字符和B串头两个字符相同则可以相连, 若存在三个串首尾相连就可以形成一个环, 让我们求出可以拼接出来的环长度平均值的最大值, 重复部分会算两次, 即A串和B串连接后得到的长度是A长度+B长度。

既然是分数形式, 试着往01分数规划方面思考, 这里的最大值也有上下界:, 最小不小于0, 每个串都长度最大是取最大值。 定义区间中点为 , 可以通过二分条件 来求解, 转化为: 而 是拼接的字符串个数, 这里则都是1, 可以省去:

那么现在就是判断是否能构成一个正环。

求环的话试着建图, 若把一个字符串当做一个节点, 点权值为当前字符串长度, 边权值为1。最大共有 个点, 若所有字符串都一样, 那就是全连接图 条边, 显然是无法通过SPFA解决的东西。

换一种建图方式, 根据题目的条件, 一个字符串除了前面两个和后面两个字符, 中间字符是什么我们没必要关心和维护, 它们的贡献只有长度。那么对于一个串, 把前两个字符和后两个字符视为两个点, 该串的长度视为这两个点之间的边权。这样即保留了原本信息, 还可以满足题目首尾相接的条件。总点数也就 个, 全连接图的话边数为 , 总时间复杂度最坏为 , 不过这种卡边界的时间可以通过一些优化来解决。

比如提前终止搜索和把队列改成栈两种方法都可以过。

const int N = 700, M = 1e6;
int n;
int h[N], e[M], ne[M], w[M], idx;
double dist[N];
bool st[N];
int q[N], cnt[N];
 
void add(int a, int b, int c) { e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++; }
 
bool check(double mid)
{
    memset(st, 0, sizeof st);
    memset(cnt, 0, sizeof cnt);
    int hh = 0, tt = 0;
    for (int i = 0; i < 676; i++)
        q[tt++] = i, st[i] = true;
    while (hh != tt)
    {
        int t = q[--tt];
        st[t] = false;
 
        for (int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            if (dist[j] < dist[t] + w[i] - mid)
            {
                dist[j] = dist[t] + w[i] - mid;
                cnt[j] = cnt[t] + 1;
                if (cnt[j] >= N)
                    return true;
                if (!st[j])
                    q[tt++] = j, st[j] = true;
            }
        }
    }
    return false;
}
 
int main()
{
    while (cin >> n, n)
    {
        memset(h, -1, sizeof h);
        idx = 0;
        char s[1010];
        for (int i = 0; i < n; i++)
        {
            cin >> s;
            int m = strlen(s) - 1;
            if (m + 1 < 2)
                continue;
            int a = (s[0] - 'a') * 26 + (s[1] - 'a'), b = (s[m - 1] - 'a') * 26 + (s[m] - 'a');
            add(a, b, m + 1);
        }
        if (!check(0))
        {
            cout << "No solution\n";
            continue;
        }
        double l = 0, r = 1000;
        while (r - l > 1e-4)
        {
            double mid = (l + r) / 2.0;
            if (check(mid))
                l = mid;
            else
                r = mid;
        }
        printf("%.2lf\n", r);
    }
 
    return 0;
}

5 数论

5.1 求组合数

5.1.1 a,b ⇐ 2000

void pre()
{
	for(int i = 0; i < N; i++)
		for(int j = 0; j <= i; j++)
		{
		    if(!j) f[i][j] = 1;
			else f[i][j] = (f[i - 1][j - 1] + f[i - 1][j]) % MOD;
		}
}

5.1.2 a,b ⇐ 1e5

typedef long long LL;
const int N = 1e5 + 10, MOD = 1e9 + 7;
int fact[N], infact[N];
int n;
 
int qmi(int a, int b, int p)
{
    int res = 1;
    while(b)
    {
        if(b & 1) res = (LL)res * a % p;
        b >>= 1;
        a = (LL)a* a % p;
    }
    return res ;
}
 
int main()
{
	fact[0] = infact[0] = 1;
	for(int i = 1; i <= N; i++)
	{
		fact[i] = (LL)fact[i - 1] * i % MOD;
		infact[i] = (LL)infact[i - 1]*qmi(i, MOD - 2, MOD) % MOD;
	}
	cin >> n;
	while(n--)
	{
		int a, b;
		cin >> a >> b;
		cout << (LL)fact[a] * infact[b] % MOD * infact[a - b] % MOD << endl;
	}
	return 0;
}

5.1.3 a,b ⇐ 1e18

算后面一个式子时也这样递归的计算, 直到最后p > a,b

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
LL p;
 
int qmi(LL a, LL b ,LL p)
{
    int res = 1;
    while(b)
    {
        if(b & 1) res = (LL) res * a % p;
        b >>= 1;
        a = (LL) a * a % p;
    }
    return res;
}
 
 
int C(LL a, LL b)
{
    int res = 1;
    for(int i = 1, j = a; i <= b; i++, j--)
    {
        res = (LL) res * j % p;
        res = (LL) res * qmi(i, p - 2, p) % p;
    }
    return res;
}
 
 
int lucas(LL a, LL b)
{
    if(a < p && b < p) return C(a,b);
    return (LL)  C(a%p, b%p)*lucas(a/p, b/p) % p;
}
 
int main()
{
    int n;
    cin >> n;
    while (n -- ){
        LL a, b;
        cin >> a >> b >> p;
        cout << lucas(a,b) << endl;
    }
    return 0;
}

5.1.4 a ⇐ 5000 且 无MOD

const int N = 5e3 + 10;
int primes[N], cnt;
bool st[N];
 
int sum[N];
 
void getprimes(int n)
{
    for(int i = 2; i <= n;i ++)
    {
        if(!st[i]) primes[cnt++] = i;
        for(int j = 0; primes[j] <= n / i; j++)
        {
            st[primes[j] * i] = true;
            if(i % primes[j] == 0) break;
        }
    }
}
 
int get(int n,int  p)
{
    int res = 0;
    while(n)
    {
        res += n / p;
        n /= p;
    }
    return res;
}
 
 
vector<int> mul(vector<int> res, int x)
{
    vector<int> c;
    int t = 0;
    for(int i = 0; i < res.size(); i++)
    {
        t += res[i] * x;
        c.push_back(t % 10);
        t /= 10;
    }
    while(t)
    {
        c.push_back(t % 10);
        t/=10;
    }
    
    return c;
}
 
int main()
{
    int a,b;
    cin >> a >> b;
    getprimes(a);
    
    for(int i = 0; i < cnt; i++)
    {
        int p = primes[i];
        sum[i] = get(a,p) - get(b,p) - get(a - b,p);
    }
        
    
    vector<int> res;
    res.push_back(1);
    for(int i = 0; i < cnt; i++)
        for(int j = 1; j <= sum[i]; j++)
            res = mul(res, primes[i]);
    
    for(int i = res.size() - 1; i >= 0; i--)
    {
        cout << res[i];
    }
    return 0;
}

5.1.5 卡特兰数

给定 n 个 0 和 n 个 1,它们将按照某种顺序排成长度为 2n 的序列,求它们能排列成的所有序列中,能够满足任意前缀序列中 0 的个数都不少于 1 的个数的序列有多少个。

输出的答案对 取模。

所有经过蓝色线到达(6,6)的路径, 在对于蓝线做对称后到达的点一定是 (5,7), 所以结果为:

typedef long long LL;
const int p = 1e9 + 7;
 
 
int qmi(int a, int b, int p)
{
    int res = 1;
    while(b)
    {
        if(b & 1) res = (LL) res * a % p;
        b >>= 1;
        a = (LL) a * a % p;
    }
    return res;
}
 
int main()
{
    int n,res = 1;
    cin >> n;
    int a = 2*n, b = n;
    for(int i = a; i > a-b; i--) res = (LL) res * i % p;
    for(int i = 1; i <= b; i++) res = (LL) res * qmi(i, p - 2, p) % p;
    
    res = (LL) res * qmi(n + 1, p - 2, p) % p;
    cout << res << endl;
    return 0;
}

5.2 质数筛

5.2.1 埃氏筛

bool st[N];
int prime[N], cnt;
for(int i = 2; i <= n;i ++)
{
	if(!st[i])
	{
		prime[cnt++] = i;
		for(int j = i + i; j <= n; j += i)
			st[j] = true;
	}
}

5.2.2 线性筛

for(int i = 2; i <= n; i++)
{
	if(!st[i]) prime[cnt++] = i;
	for(int j = 0; prime[j] < n / i; j++)
	{
		st[prime[j] * i] = true;
		if(i % prime[j] == 0) break;
	}
}

5.2.3 分解质因数

int n;
int main()
{
    cin >> n;
    while(n--)
    {
        int x;
        cin >> x;
        for(int i = 2; i <= x / i; i++)
        {
            if(x % i == 0)
            {
                int s= 0;
                while(x % i == 0)
                {
                    x /= i;
                    s++;
                }
                cout << i << " " << s << endl;
            }
        }
        if(x > 1) cout << x << " " << 1 << endl;
        cout << endl;
    }
    return 0;
}

5.2.4 阶乘分解质因数

int main()
{
    int n;
    cin >> n;
    for (int i = 2; i <= n; i++)
    {
        bool isprime = true;
        for (int j = 2; j <= i / j; j++)
            if (i % j == 0)
            {
                isprime = false;
                break;
            }
        if (isprime)
        {
            int cnt = 0;
            for (int k = i; k <= n; k *= i)
                cnt += n / k;
            cout << i << " " << cnt << endl;
        }
    }
    return 0;
}

5.3 约数

5.3.1 求一个数的所有约数

从小到大判断, 如果当前数 能 整除n 就是 n的约数。 根据约数性质, 我们可以只枚举小的约数 只枚举到 n / i, 且 当 i!=n/i 时, 再把n/i(对应的大约数)放入结果

vector<int> get_divisions(int x)
{
    vector<int> res;
    for(int i = 1; i <= x / i; i++)
    {
        if(x % i == 0)
        {
            res.push_back(i);
            if(i != x/i)
                res.push_back(x/i);
        }
    }
    sort(res.begin(), res.end());
    return res;
}

5.3.2 约数个数

给定 n 个正整数 ai,请你输出这些数的乘积的约数个数,答案对 10^9+7 取模。

int 范围内约数个数最多的数有 1500 个左右

int main()
{
    cin >> n;
    unordered_map<int,int> m;
    while (n -- ){
        int x;
        cin >> x;
        for(int i = 2; i <= x / i; i++)
            while(x % i == 0)
            {
                x/= i;
                m[i]++;
            }
        if(x > 1) m[x]++;
    }
    long long res = 1;
    for(auto i : m)
    {
        res = res * (i.second + 1) % MOD;
    }
    cout << res << endl;
    return 0;
}

5.3.3 约数之和

image-20230503223855984

for(auto i : m)
{
	int p = i.first, a = i.second;
	long long t = 1;
	while(a--) t = (t * p + 1) % MOD;
	res = res * t % MOD;
}
cout << res << endl;

5.3.4 最大公约数

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

5.4 欧拉函数

5.4.1 O(sqrt(n))

int get_eurl(int n)
{
	res = n;
	for(int i = 2; i <= n / i;i ++)
	{
		if(n % i == 0)
		{
			res *= (i - 1) / i;
			while(n % i == 0)
				n /= i;
		}
	}
	if(n > 1) res *= (n - 1) / n;
	return res;

5.4.2 筛法 O(n)

LL get_eurl(int n)
{
	phi[1] = 1;
	for(int i = 2; i <= n;i ++)
	{
		if(!st[i])
		{ 
			primes[cnt++] = i;
			phi[i] = i - 1;
		}
		for(int j = 0; primes[j] <= n / i; j++)
		{
			st[primes[j] * i] = true;
			if(i % primes[j] == 0)
			{
				phi[primes[j] * i] = phi[i] * primes[j];
				break;
			}
			phi[primes[j] * i] = phi[i] * (primes[j] - 1);
		}
	}
	LL res = 0;
	for(int i = 1; i <= n ;i ++) res += phi[i];
	return res;
}

5.5 扩展欧几里得与中国剩余定理

5.5.1 扩展欧几里得求ab 满足 ax+by=gcd(a,b)

void exgcd(int a, int b, int &x, int &y)
{
    if(!b)
    {
        x = 1, y = 0;
        return;
    }
 
    exgcd(b, a % b, x, y);
    
    int t = x;
    x = y, y = t - a/b*y;
}

5.5.2 求线性同余方程的特解

image-20230503224425207

typedef long long LL;
 
int exgcd(int a, int b, int &x, int &y)
{
    if(!b)
    {
        x = 1, y = 0;
        return a;
    }
    int t = exgcd(b, a % b, y, x);
    y = y - a/b * x;
    return t;
}
 
 
int n;
 
int main()
{
    cin >> n;
    while(n--)
    {
        int a,m,b,x,y;
        scanf("%d%d%d", &a, &b, &m);
        int d = exgcd(a,m,x,y);
        if(b % d) cout << "impossible\n";
        else printf("%d\n", (LL)x * (b / d) % m);
    }
    return 0;
}

5.5.3 中国剩余定理的通解

找到硬币总数X, 满足分k次,每次分出来Mi个, 最后剩下Ai个。 转化为数学表达: 显然, 是中国剩余定理的模板题。

template <typename T>
inline T exgcd(T a, T b, T &x, T &y)
{
    if (!b)
    {
        x = 1, y = 0;
        return a;
    }
    T t = exgcd(b, a % b, y, x);
    y -= a / b * x;
    return t;
}
 
const int N = 1e2 + 10;
typedef long long LL;
LL a[N], b[N];
 
int main()
{
 
    int T;
    cin >> T;
    int cnt = 0;
    while (T--)
    {
        LL n, a1, b1;
        cin >> n;
	    for(int i = 0; i < n;i ++) cin >> a[i];
        for(int i = 0; i < n;i ++) cin >> b[i];
        a1 = a[0], b1 = b[0];
        bool flag = true;
        for(int i = 1; i < n; i++)
        {
            LL k1, k2;
            LL d = exgcd(a1, -a[i], k1, k2);
            LL lcm = a1 / d * a[i];
            if ((b[i] - b1) % d) // 若不满足定理使用条件说明无解
                flag = false;
            k1 *= (b[i] - b1) / d; // 扩大
            LL t = a[i] / d;
            k1 = (k1 % t + t) % t; // 保证为最小解
            b1 = k1 * a1 + b1; // 更新
            a1 = abs(lcm);
        }
        if (!flag)
            cout << "Case " << ++cnt << ": " << -1 << endl;
        else if (b1 != 0)
            cout << "Case " << ++cnt << ": " << (b1 % a1 + a1) % a1 << endl;
        else // 0不能算结果哦
            cout << "Case " << ++cnt << ": " << b1 + a1 << endl;
    }
 
    return 0;
}

5.5.4 中国剩余定理的解个数

template <typename T>
inline T exgcd(T a, T b, T &x, T &y)
{
    if (!b)
    {
        x = 1, y = 0;
        return a;
    }
    T t = exgcd(b, a % b, y, x);
    y -= a / b * x;
    return t;
}
const int N = 1e2 + 10, MOD = 100003;
// int f[N][N];
long long n, m;
long long a[12], b[12];
int main()
{
    int T;
    cin >> T;
    while (T--)
    {
        cin >> n >> m;
        for(int i = 0; i < m; i++) a[i] = readInt();
        for(int i = 0; i < m; i++) b[i] = readInt();
        long long a1 = a[0], b1 = b[0], k1, k2;
        bool flag = true;
        for(int i = 1; i < m; i++)
        {
            long long d = exgcd(a1, -a[i], k1, k2);
            if ((b[i] - b1) % d)
                flag = false;
            // 这里是优化为最小整数解, 提高速度
            k1 *= (b[i] - b1) / d;
            long long t = a[i] / d;
            k1 = (k1 % t + t) % t;
            
            b1 = k1 * a1 + b1;
            a1 = abs((a1 / d) * a[i]);
        }
        if (flag && b1 <= n)
        {
            b1 = (b1 % a1 + a1) % a1;
            long long res = (n - b1) / a1; // 这是除去 最小非负整数x 之后的其他解
            if (b1 != 0) // 若 x 不为0则算一个解
                res++;
            cout << res << endl;
        }
        else
            cout << 0 << endl;
    }
    return 0;
}

5.6 高斯消元

5.6.1 解线性方程组

模拟正常用行列式高斯消元的过程

  1. 第一列中选择绝对值最大的一行
  2. 放到第一行上
  3. 将该列系数化为1
  4. 循环123到最后一列
  5. 之后逆序求解
const int N = 1e2 + 10;
const double eps = 1e-6;
 
int n;
double a[N][N];
 
int gauss()
{
    int c,r;
    for(c = 0,r = 0; c < n; c++)
    {
        int t = r;
        for(int i = r; i < n; i++)
            if(fabs(a[i][c]) > fabs(a[t][c]))
                t = i;
        
        if(fabs(a[t][c]) < eps) continue;
        
        for(int i = c; i < n+1; i++) swap(a[t][i], a[r][i]); // 交换到第一行
        for(int i = n; i >= c; i--) a[r][i] /= a[r][c]; // 系数化为1
        for(int i = r + 1; i < n; i++) // 把其他行的删去
            if(fabs(a[i][c]) > eps)
                for(int j = n; j >= c; j--)
                    a[i][j] -= a[r][j] * a[i][c]; 
        
        r++;
    }
    
    if(r < n)
    {
        for(int i = r; i < n;i ++)
            if(fabs(a[i][n]) > eps) return 2;
        return 1;
    }
    
    for(int i = n - 2; i >= 0; i--)
        for(int j = i + 1; j < n; j++)
        {
            a[i][n] -= a[j][n] * a[i][j];
        }
    
    
    return 0;
}
 
 
int main()
{
    cin >> n;
    for(int i = 0; i < n;i ++)
        for(int j = 0; j < n + 1;j ++)
        {
            cin >> a[i][j];
        }
    int t = gauss();
    if(t == 0)
    {
        for(int i = 0; i < n;i ++) 
            if(a[i][n] != 0)
                printf("%.2lf\n", a[i][n]);
            else
                printf("0.00\n");
    }
    else if(t == 1) cout << "Infinite group solutions";
    else cout << "No solution";
}

5.6.2 解异或线性方程组

类似于高斯消元, 把系数的算术变换改为异或运算, 实际上就是不进位的加法。

  1. 依然是寻找最大值, 不过这里找到一个1就行
  2. 交换到第一行
  3. 不需要去化系数, 本身就是1
  4. 将第一行式子与其他行式子相异或, 让该列为0
  5. 重复以上操作直到最后一个
  6. 然后逆向求解, 将一行中其他未知数的系数异或为0即可
const int N = 110;
int a[N][N];
int n;
 
int gauss()
{
    int c,r;
    for(c = 0, r = 0; c < n; c++)
    {
        int  t =  -1;
        for(int i = r; i < n; i++)
            if(a[i][c]){t = i; break;}
        if(t == -1) continue;
        
        for(int i = 0; i < n + 1; i++) swap(a[r][i], a[t][i]);
        for(int i = r + 1; i < n; i++)
            if(a[i][c])
                for(int j = 0; j < n + 1; j++)
                    a[i][j] ^= a[r][j];
        r++;
    }
    if(r < n)
    {
        for(int i = r; i < n; i++)
            if(a[r][n]) return 2;
        return 1;
    }
    for(int i = n  - 1; i >= 0; i--)
        for(int j = i + 1; j < n; j++)
            if(a[i][j])
                a[i][n] ^= a[j][n];
    return 0;
}
 
int main()
{
    cin >> n;
    for(int i = 0; i < n; i++)
        for(int j = 0; j < n + 1; j++)
            cin >> a[i][j];
            
    int t = gauss();
    if(t == 0)
    {
        for(int i = 0; i < n;i ++) cout << a[i][n] << endl;
    }
    else if(t == 1) cout << "Multiple sets of solutions";
    else cout << "No solution";
    return 0;
}

5.7 博弈论

5.7.1 n堆石子, 无限拿

定理:

int main()
{
    int x;
    cin >> n;
    cin >> x;
    n -= 1;
    while(n--)
        x ^= readInt();
    if(x) cout << "Yes\n";
    else cout << "No\n";
    
}

5.7.2 1堆石子, 共有n个石子, 俩人拿1~k个

有一堆石子共有N个。A B两个人轮流拿,A先拿。每次最少拿1颗,最多拿K颗,拿到最后1颗石子的人获胜。

可以把石子分成 m 堆, 每堆 k 个, 最后可能会有一堆 ⇐ k 的。 若存在 一堆⇐k, 则先手可以取走 这一堆, 剩下的全是 m堆里 每堆数量都是k个, 转化为Nim游戏, 此时第一个取的人必败。

但有个要求, 剩下的堆数不能为1, 否则后手一定胜利。 这里把每堆分为 k+1 个石子, 这样就符合要求。

故若 n % (k + 1) > 0时,先手必赢。

5.7.3 n堆石子按顺序, 俩人可以拿石子或放到下一堆

有一个 级台阶的楼梯,每级台阶上都有若干个石子,其中第 级台阶上有 个石子。

两位玩家轮流操作,每次操作可以从任意一级台阶上拿若干个石子放到下一级台阶中(不能不拿)。

已经拿到地面上的石子不能再拿,最后无法进行操作的人视为失败。

问如果两人都采用最优策略,先手是否必胜。

根据Nim游戏定理, 让所有可以拿的石子数相异或为0就可以先手必胜。

但这里除了台阶1, 其他台阶的石子只能合并到下一个台阶上。

怎样保证自己一定有石子拿呢? 把奇数的台阶看做Nim游戏, 全异或为0时必输。 若对手移动偶数台阶的数, 则我们把他移动过的再移动到下一个台阶, 不会影响奇数台阶的性质。 若队友移动奇数台阶的数, 根据Nim游戏, 我们总能找到一种移动方案来使奇数台阶异或为0。

int main()
{
    int n, x;
    cin >> n;
    cin >> x;
    for(int i = 2; i <= n;i++)
    {
        int t;
        cin >> t;
        if(i % 2) x ^= t;
    }
    if(x) cout << "Yes\n";
    else cout << "No\n";
    return 0;
}

5.7.4 n堆石子, 每次只能拿在集合S中的个数

给定 堆石子以及一个由 个不同正整数构成的数字集合 。

现在有两位玩家轮流操作,每次操作可以从任意一堆石子中拿取石子,每次拿取的石子数量必须包含于集合 ,最后无法进行操作的人视为失败。

当SG值为0时, 从该点先手一定会输。

再根据有向图游戏的和性质, 若所有堆石子数异或后的结果为0, 则先手必输。

计算所有SG函数时使用记忆化搜索, 也算枚举了所有选择的可能, 相当暴力的做法, 复杂度为指数级别。

const int N = 1e2 + 10, M = 1e5;
int s[N], f[M];
int k,n;
 
int sg(int x)
{
    if(f[x] != -1) return f[x];
    unordered_set<int> S;
    for(int i = 0; i < k; i++)
        if(x >= s[i]) S.insert(sg(x - s[i]));
    for(int i = 0;; i++)
        if(!S.count(i))
            return f[x] = i;
    
}
 
 
int main()
{
    memset(f, -1, sizeof f);
    cin >> k;
    for(int i = 0; i < k; i++) cin >> s[i];
    cin >> n;
    int res = 0;
    for(int i = 0; i < n; i++)
    {
        int x;
        cin >> x;
        res ^= sg(x);
    }
    if(res) cout << "Yes\n";
    else cout << "No\n";
    return 0;
    
}

5.7.5 两堆石子, 取任意多个或两堆取相同的, 必输态差值为递增序列

从小到大枚举必输态: 第一种(0,0) 第二种(1,2) 第三种(3,5) 第四种 (4 ,7) 第五种(6,10) 第六种 (8,13) 第七种 (9 , 15) 第八种 (11 ,18) 第n种 (a[k],b[k])

可以发现他们之间的差值是递增序列1234567 第一个数是前面未出现过的最大整数。 既然是差值递增序列且第一个数有独特的出现规则, 可否通过差值和第一个数来判断该局势是否为必输呢?

拿第一个数/差值可以发现, 其结果是固定的:

即

故满足这个条件就是必输局面 转换一下方便判断: 0.618是黄金分割率, 1.618可以用 (sqrt(5.0) + 1) / 2 高精度表示

const int N = 1e2 + 10, M = 1e7, MOD = 100003;
int n, m, k;
int s[N], f[N];
 
int main()
{
    int n, m;
    double f = (sqrt(5.0) + 1.0) / 2.0;
    while (cin >> n >> m)
    {
        if (n > m)
            swap(n, m);
        if (n == (int)((m - n) * f))
            cout << 0 << endl;
        else
            cout << 1 << endl;
    }
 
    return 0;
}

5.7.6 1堆石子, 共n个, 取1~任意但不能直接取完, 下一次不能超上次2倍, 必输态为斐波那契数列

1堆石子有n个,两人轮流取.先取者第1次可以取任意多个,但不能全部取完.以后每次取的石子数不能超过上次取子数的2倍。

const int N = 1e2 + 10, M = 1e7, MOD = 1e9;
typedef long long LL;
LL n;
LL f[N];
int main()
{
    f[1] = 1, f[2] = 1;
    for (int i = 3; i <= 56; i++)
        f[i] = f[i - 1] + f[i - 2];
    while (cin >> n && n)
    {
        if ((*lower_bound(f, f + 56, n)) == n)
            cout << "Second win\n";
        else
           cout << "First win\n";
 
    }
    return 0;
}

5.7.7 环形硬币, 每次取连续 1~k 个

首先对于一串硬币, 没有首尾相接的时候: 1 2 3 4 5 6 7 8 9 10 (k = 2) 我们只需要先手把中间的 5 6 拿掉 1 2 3 4 7 8 9 10 之后对称着拿就行, 总是胜的。 k = 1时则只能是奇数时胜。

首尾相接后, 第一个操作之后就会化为一串硬币, 同理: 后手只要把中间的拿掉就赢。

所有我们只有两种情况能赢:

  1. k >= n(一次拿完)
  2. k == 1 && n % 2(n为奇数且k为1)
int main()
{
 
    int T;
    cin >> T;
    for (int kase = 1; kase <= T; kase++)
    {
        int n, k;
        cin >> n >> k;
        if (n <= k || (n % 2 && k == 1))
            cout << "Case " << kase << ": "
                 << "first\n";
        else
            cout << "Case " << kase << ": "
                 << "second\n";
    }
 
    return 0;
}

5.7.8 N堆, 每堆S[i]个, 拿走任意个或分成两小堆

Alice和Bob轮流取N堆石子,每堆S[i]个,Alice先,每一次可以从任意一堆中拿走任意个石子,也可以将一堆石子分为两个小堆。

每分割出去的部分是新的游戏,显然需要用到SG函数。 但跟[集合 SG函数](app://obsidian.md/index.html#集合 SG函数)的不一样, 无法通过枚举来打表, 需要找规律: 分割堆数用 sg[j] ^ sg[i-j]

int main()
{
    sg[0] = 0;
    sg[1] = 1;
    for (int i = 2; i <= N; i++)
    {
        bool st[N] = {};
        for (int j = 0; j <= i; j++)
        {
            st[sg[j]] = true;
            if (j && j != i)
                st[sg[j] ^ sg[i - j]] = true;
        }
        int j = 0;
        while (st[j])
            j++;
        sg[i] = j;
    }
    for (int i = 1; i <= 1000; i++)
    {
        cout << sg[i] << " ";
        if (i % 10 == 0)
            cout << endl;
    }
    return 0;
}

image-20230503230518507

可以看出, 当 n%4 == 0 时, 会和 n-1 的数互换: sg[x] = x - 1(x%4 == 0) sg[x] = x + 1(x%4==3)

int sg(int x)
{
    if (x % 4 == 0)
        return x - 1;
    else if (x % 4 == 3)
        return x + 1;
    else
        return x;
}
 
int main()
{
 
    int T = readInt();
    while (T--)
    {
        int n = readInt(), res = 0;
        while (n--)
            res ^= sg(readInt());
        if (res)
            printf("Alice\n");
        else
            printf("Bob\n");
    }
 
    return 0;
}

5.7.9 棋盘右上角到左下角,往左移一格或往下一一格 | 两堆石子, 取一个或两堆都取一个

最近帅气的羊羊很闲,找到了美丽的驼驼下棋。 棋盘的大小是n∗m,羊羊突然想到一个新的玩法,首先有个卒放在棋盘的右上角(1,m)的位置。 每一次羊羊或者驼驼可以将这个卒向左移一步或者向下移一步,或者向左下移一步 谁不能移动谁就输了。羊羊先移动棋子卒,羊羊会赢吗?假设玩家都是最优决策。

巴什博奕思路: 两堆 n,m 的石子, 可以每堆取一个或者同时取两堆中各一个。 由于是一个一个取,显然和奇偶性有关。 若只有一堆, 则奇数先手必胜。 若有两堆, 全为偶数时, 先手必败,因为后手总能把数量都维持成偶数。 一奇一偶时, 先手必胜,取一个奇数的就变成全偶数必输。 全奇时, 取两个变成全偶局面。

即含有奇数时先手必胜。 这里nm初始减一, 故为含有偶数时先手必胜。

PN图:

必败点(P点) :前一个选手(Previous player)将取胜的位置称为必败点。 必胜点(N点) :下一个选手(Next player)将取胜的位置称为必胜点。

实际上就是按照规则画图;有以下三条规则:

  1. 每个图的末状态均为必败点P
  2. 所有能够一步到达必败点的都是必胜点N
  3. 所有能够一步到达必胜点的都是必败点 P 画图的时候,选定对角线定点里面的那个点位末状态,题上的说明,那么一般找的规则就是从最底边上,还有最开始的那一列开始确定P还是N点,这样很方便的看出,当行列都是奇数的时候,那么一定会必败点。
int main()
{
 
    int n, m;
    while (cin >> n >> m && (n || m))
    {
        if (n % 2 == 0 || m % 2 == 0)
            cout << "Wonderful!\n";
        else
            cout << "What a pity!\n" ;
    }
 
    return 0;
}

5.7.10 n堆, 无限取, 输出先手能取的方案数

若res不为0, 则一定存在一种取数方式, 将取完的res变为0: 设取之前res = x, 一定存在a[i]的二进制中, x的最高位为1, 令 a[i] = x ^ a[i], 那么这个数会比原来更小, 少的这一部分便是拿走的数量。 之后带入到原来的式子中会得到:x ^ x = 0, 故到达必败态。

这里a[i]并不是递增顺序, 故有可能 a[i] ^ x > a[i]', 拿的过多, 不成立。 故需要加上 x^a[i] < a[i]‘ 的条件。

int main()
{
 
    while (cin >> n && n)
    {
        int res = 0, a[110], cnt = 0;
        for (int i = 0; i < n; i++)
            res ^= a[i] = readInt();
        if (res == 0)
            cout << "0\n";
        else
        {
            for (int i = 0; i < n; i++)
            {
                int k = res ^ a[i];
                if (k < a[i])
                    cnt++;
            }
            cout << cnt << endl;
        }
    }
    return 0;
}

5.8 杂项

5.8.1 大数取模

给出一个数 ,判断是否是中某些数的的倍数

可得:1234%m -⇒ (1200%m + 34%m)%m -⇒ ((12*100)%m + (3*10%m+4%m)%m)%m 也就是说可以拆分为十进制表示 abcd→a*1000 + b*100 + c*10 + d 对他们每个数取模求和的结果再取一次模就是该大数的模

bool getmod(string &s, int mod)
{
    int res = 0;
    for(int i = 0; i < s.size(); i++)
    {
        res = (res * 10 + s[i] - '0') % mod;
    }
    if(res) return false;
    return true;
}

5.8.2 求矩形交并集面积

求交集面积需要先找出 即两个矩形左侧边的最小值, 右侧边的最大值, 上侧边的最小值, 下侧边的最大值。然后再求面积。

并集面积可以在求出交集面积后, 将两个矩形面积相加再减去交集面积。

#include <bits/stdc++.h>
using namespace std;
int x,y,a,b;
int main()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout << std::fixed << std::setprecision(10) << "\n";
    int T;
    cin >> T;
    while(T--)
    {
 
        double ans = 0;
        cin >> x >> y >> a >> b;
        for(auto c : {0,x})
            for(auto d : {0,y})
            {
                int xl = max(0, min(a,c));
                int xr = min(x, max(a,c));
                int yl = max(0, min(b,d));
                int yr = min(y, max(b,d));
                int inner = abs(xl - xr) * abs(yl - yr);
                int uni = x*y + abs(a-c)*abs(b-d) - inner;
                double res = 1. * inner / uni;
                ans = max(res, ans);
            }
        cout << ans << "\n";
            
    }
    return 0;
}

6 字符串

6.1 最小循环重构

也就是循环重构, 那我们这题就是求最小字典序的循环重构串。

image-20230503163120928

用i来枚举当前最小串的起点, j来枚举新串的起点, 用k来做偏移量 s[i + k], s[j + k] 判断两个串谁的字典序小。

int getmin(string s)
{
    int i = 0, j = 1;
    while(i < n && j < n)
    {
        int k = 0;
        while(k < n && s[i + k] == s[j + k]) k++;
        if(k == n) break;
        if(s[i + k] > s[j + k]) i += k + 1 + (i==j);
        else j += k + 1 + (i==j);
    }
    return min(i,j);
}

6.2 KMP求字串数量

const int N = 1e7 + 10;
char s[N],p[N];
int ne[N];
int main()
{
    int n,m;
    cin >> n >> p + 1 >> m >> s + 1;
    
    for(int i = 2, j = 0; i <= n; i++)
    {
        while(j && p[j + 1] != p[i]) j = ne[j];
        if(p[i] == p[j + 1]) j++;
        ne[i] = j;
    }
    
    for(int i = 1, j = 0; i <= m; i++)
    {
        while(j && s[i] != p[j + 1]) j = ne[j];
        if(s[i] == p[j + 1]) j++;
        if(j == n)
            cout << i - n<< " ";
    }
}

6.3 Tire树统计单词出现次数

const int N = 2e4 + 10;
int son[N][26],cnt[N], idx;
int n;
char str[N];
 
void insert(char str[])
{
    int p = 0;
    for(int i = 0; str[i]; i++)
    {
        int u = str[i] - 'a';
        if(!son[p][u]) son[p][u] = ++idx;
        p = son[p][u];
    }
    cnt[p]++;
}
 
int query(char str[])
{
    int p = 0;
    for(int i = 0; str[i]; i++)
    {
        int u = str[i] - 'a';
        if(!son[p][u]) return 0;
        p = son[p][u];
    }
    return cnt[p];
}
 
 
int main()
{
    cin >> n;
    while(n--)
    {
        char s[2];
        cin >> s >> str;
        if(s[0] == 'I') insert(str);
        else cout << query(str) << endl;
    }
    return 0;
}

7 数据结构

7.1 单/双链表

7.1.1 单链表

const int N = 100020;
int head, e[N],ne[N],idx;
 
void init()
{
    head = -1;
    idx = 0;
}
 
void add_to_head(int k)
{
    e[idx] = k, ne[idx] = head, head = idx++;
}
void add(int k, int dex)
{
    e[idx] = dex, ne[idx] = ne[k], ne[k] = idx++;
}
void remove(int k)
{
    ne[k] = ne[ne[k]];
}
 
 
int main()
{
    int n;
    scanf("%d",&n);
    init();
    while(n--)
    {
        char a;
        int k;
        cin >> a;
        if(a == 'H')
        {
            cin >> k;
            add_to_head(k);
        }
        if(a == 'I')
        {
            int dex;
            cin >> k >> dex;
            add(k-1,dex);
        }
        if(a == 'D')
        {
            cin >> k;
            if(k == 0) head = ne[head];
            else remove(k-1);
        }
    }
    
    for(int i = head; i != -1; i = ne[i]) cout << e[i] << ' ';
}

7.1.2 双链表

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e6;
int e[N], l[N], r[N], idx;
 
void init()
{
    l[1] = 0;
    r[0] = 1;
    idx = 2;
}
 
 
void add(int k, int x)
{
    e[idx] = x;
    l[idx] = k;
    r[idx] = r[k];
    l[r[k]] = idx;
    r[k] = idx++;
}
 
void remove(int k)
{
    l[r[k]] = l[k];
    r[l[k]] = r[k];
}
 
 
int main()
{
    init();
    int n;
    cin >> n;
    while (n -- )
    {
        char s[3];
        int k,x;
        cin >> s;
        if(s[0] == 'L')
        {
            cin >> x;
            add(l[0], x);
        }
        if(s[0] == 'R')
        {
            cin >> x;
            add(l[1], x);
        }
        if(s[0] == 'D')
        {
            cin >> k;
            remove(k + 1);
        }
        if(s[0] == 'I' && s[1] == 'R')
        {
            cin >> k >> x;
            add(k + 1, x);
        }
        if(s[0] == 'I' && s[1] == 'L')
        {
            cin >> k >> x;
            add(l[k + 1], x);
        }
    }
    
    for(int i = r[0]; i != 1; i = r[i])
    {
        cout << e[i] << " ";
    }
    return 0;
}

7.2 并查集

实现两个操作:

  1. 合并两个集合
  2. 查询某个元素的祖宗节点
    • 路径压缩优化
    • 按秩合并
    • 俩加一起是 , 基本为

扩展运用:

  1. 记录每个集合的大小, 和本身绑定, 直接绑定到祖宗节点上
  2. 每个点到根节点的距离, 因为每个点都不同, 因此需要绑定到每个节点上
    • 维护多类集合
  3. 链表问题, 染色一段但后一段会把前一段覆盖

7.2.1 捆绑销售, 选A需选BCDE..

需要求代价不超过 w 的最大价值, 是01背包问题。但这里多了一个要求, 有的商品拿了一个需要拿一系列有关的。

可以先用并查集处理, 把相关的缩成一个点, 枚举时只考虑 的点即可。

const int N = 1e4 + 10;
int w[N], f[N], v[N], res[N];
 
int find(int u)
{
    if (u != f[u])
        f[u] = find(f[u]);
    return f[u];
}
 
void merge(int a, int b)
{
    int pa = find(a), pb = find(b);
    f[pb] = f[pa];
    v[pa] += v[pb];
    w[pa] += w[pb];
}
 
int main()
{
    int n, m, t;
    cin >> n >> t >> m;
    for (int i = 1; i <= n; i++)
    {
        cin >> v[i] >> w[i];
        f[i] = i;
    }
 
    while (t--)
    {
        int a, b;
        cin >> a >> b;
        int pa = find(a), pb = find(b);
        if (pa != pb)
            merge(a, b);
    }
 
    for (int i = 1; i <= n; i++)
    {
        if (f[i] != i)
            continue;
        for (int j = m; j >= v[i]; j--)
            res[j] = max(res[j], res[j - v[i]] + w[i]);
    }
    cout << res[m] << endl;
    return 0;
}

7.2.2 判断等式是否不成立, 范围大需离散化

const int N = 3e5 + 10;
typedef pair<int, int> PII;
int f[N];
int n;
unordered_map<int, int> S;
 
struct Query {
    int x, y, e;
}query[N];
 
int find(int x)
{
    if(f[x] != x) f[x] = find(f[x]);
    return f[x];
}
 
void merge(int a, int b)
{
    int pa = find(a), pb = find(b);
    f[pb] = f[pa];
}
 
int get(int x)
{
    if(S.count(x) == 0) S[x] = ++n;
    return S[x];
}
 
int main()
{
    int T;
    cin >> T;
    
    while(T--)
    {
        bool flag = true;
        n = 0;
        S.clear();
        int t;
        cin >> t;
 
        for(int i = 0; i < t;i ++)
        {
            int a, b, p;
            cin >> a >> b >> p;
            query[i] = {get(a), get(b), p};
        }
 
        for(int i = 1; i <= n; i++) f[i] = i;
        
        for(int i = 0; i < t; i++)
            if(query[i].e == 1) merge(query[i].x, query[i].y);
        
        for(int i = 0; i < t;i ++)
        {
            if(query[i].e == 1) continue;
            int a = query[i].x, b = query[i].y;
            int pa = find(a), pb = find(b);
            if(pa == pb)
            {
                flag = false;
                break;
            }
        }
        
        if(flag) cout << "YES\n";
        else cout << "NO\n";
    }
    return 0;
}

7.2.3 接竹竿式操作+查询

有 艘战舰,也依次编号为 ,其中第 号战舰处于第 列。

有 条指令,每条指令格式为以下两种之一:

  1. M i j,表示让第 号战舰所在列的全部战舰保持原有顺序,接在第 号战舰所在列的尾部。
  2. C i j,表示询问第 号战舰与第 号战舰当前是否处于同一列中,如果在同一列中,它们之间间隔了多少艘战舰。

若只有求是否在一列中的操作时, 该题就是简单的并查集问题。求距离时, 定义 d[i] 为 i 点到 p[i] 的距离, s[i] 为 i 所在集合的点数量。

这样对于求同一列的 a,b 两点距离时, 就可以用 abs(d[b] - d[a]) 求得, 前缀和思想。

当 a 接到 b 后面时, d[pa] = s[pb] 将 a 集合头节点的祖先距离设置为到 pb 的距离, 此时因为 a 集合内的点头节点都是 pa, 故当进行 find 操作时, 会把 d[pa] 加到 d[a] 上, 从而实现更新 a 节点的祖先距离。

同时也要把 s[pb] += s[pa] 因为集合合并后, 点数量增加。

using namespace std;
const int N = 3e4 + 10;
int p[N], dist[N], s[N];
int n;
 
int find(int x)
{
    if(x != p[x]) 
    {
        int root =  find(p[x]);
        dist[x] += dist[p[x]];
        p[x] = root;
        
    }
    return p[x];
}
 
void merge(int a, int b)
{
    int pa = find(a), pb = find(b);
    if(pa == pb) return;
    p[pa] = pb;
    dist[pa] = s[pb];
    s[pb] += s[pa];
    
}
 
int main()
{
    cin >> n;
    for(int i = 0; i < N; i++)
        p[i] = i, s[i] = 1;
    while(n--)
    {
        char c[2];
        int a, b;
        cin >> c >> a >> b;
        if(c[0] == 'M')
            merge(a,b);
        else
        {
            int pa = find(a), pb = find(b);
            if(pa != pb) cout << -1 << endl;
            else cout << max(abs(dist[a] - dist[b]) - 1, 0) << endl;
        }
            
    }
    
    return 0;
}

7.2.4 带权并查集 区间前缀和处理

既然是区间问题, 可以定义 为 的数中有多少个1, 且我们没必要真的统计有多少个, 只需要记录当前的奇偶属性, 故可以 。

进一步分析可得, 若 有奇数个1, 即 为奇数。根据奇偶性质, 同奇同偶为偶数, 不同为奇数的性质, 若差为奇数, 说明 的奇偶性不同。 反过来, 若有偶数个1, 则 和 奇偶性相同。

于是问题转化为, 给 m 个 值, 判断哪一步构成了自环。即当 和 奇偶性既相同又不同时出错。

具体实现思想是, 定义 为 点相对于 根节点的关系, 该题中就是0, 1, 0代表同类, 1代表不同类。

接着开始处理询问, 若给出的 为偶数, 即是同一类, 分两种情况:

  • 在同一集合内, , 此时若 为 0 则正确, 为 1 则冲突
  • 不在同一集合内, 将其合并即可, 合并时需要确定 连向 的边的权值, 这里为了保证是同一类, 即权值和为偶数, , 因此只需要连一条 的边即可。 若给出的 为奇数, 即是不同类, 也分两种情况:
  • 在同一集合内, 此时若 为 1 则正确, 为 0 则冲突
  • 不在一集合内, 合并时 连向 的边需要保证为不同类, 即权值和为奇数, 满足 , 故 为 。
const int N = 5e4 + 10;
unordered_map<int,int> S;
int f[N], d[N], n;
 
int find(int x) {
    if(x != f[x]) 
    {
        int root = find(f[x]);
        d[x] = d[x] ^ d[f[x]];
        f[x] = root;
    }
    return f[x];
}
 
int get(int x)
{
    if(S.count(x) == 0) S[x] = ++n;
    return S[x];
}
 
int main()
{
    int m, res = 0;
    cin >> m;
    
    for(int i = 1; i < N ;i ++) f[i] = i, d[i] = 0;
    cin >> m;
    res = m;
    for(int i = 1; i <= m;i ++)
    {
        int a,b;
        string type;
        
        cin >> a >> b >> type;
        a = get(a - 1), b = get(b);
        
        int t = 0;
        if(type == "odd") t = 1;
        
        int pa = find(a), pb = find(b);
        if(pa == pb)
        {
            if((d[a] ^ d[b]) != t)
            {
                res = i - 1;
                break;
            }
        }
        else {
            d[pa] = d[a] ^ d[b] ^ t;
            f[pa] = pb;
        }
    }
    
    cout << res << endl;
    return 0;
}

7.2.5 扩展域求带权关系

扩展域的方法中, 集合内不再是一个点, 而是一种条件。如 这样一个条件, 而在一个集合内的条件其之间必然是相互成立。

因此对于奇偶游戏一题, 可以定义 为 x 是偶数, 定义为 x 是奇数。那么对于 同类这个操作就可以被翻译为:

  • 如果存在 与 在同一集合内, 则不成立
  • 合并 与 , 合并 不同类时:
  • 如果存在 与 在同一集合内, 则不成立
  • 合并 与 , 合并 与
const int N = 5e4 + 10, base = N / 2;
unordered_map<int,int> S;
int f[N], n;
 
int find(int x) {
    if(x != f[x]) f[x] = find(f[x]);
    return f[x];
}
 
int get(int x)
{
    if(S.count(x) == 0) S[x] = ++n;
    return S[x];
}
 
int main()
{
    int m, res = 0;
    cin >> m;
    
    for(int i = 1; i < N ;i ++) f[i] = i;
    cin >> m;
    res = m;
    for(int i = 1; i <= m;i ++)
    {
        int a,b;
        string type;
        
        cin >> a >> b >> type;
        a = get(a - 1), b = get(b);
        
        if(type == "even")
        {
            if(find(a + base) == find(b))
            {
                res = i - 1;
                break;
            }
            f[find(a)] = find(b);
            f[find(a + base)] = find(b +base);
        }
        else {
            if(find(a) == find(b))
            {
                res = i - 1;
                break;
            }
            f[find(a + base)] = find(b);
            f[find(a)] = find(b + base);
        }
    }
    
    cout << res << endl;
    return 0;
}

7.3 树状数组和线段树

7.3.1 点修改+区间查询

若仅仅只是修改一个数+求前缀和/异或和 等符合前缀和性质的操作, 就可以用树状数组解决:

int lowbit(int x)
{
    return x & -x;
}
 
void add(int x, int c)
{
    for(int i = x; i <= n; i+= lowbit(i)) t[i] += c; 
}
 
LL sum(int x)
{
    LL res = 0;
    for(int i = x; i; i -= lowbit(i)) res += t[i];
    return res;
}

若是 L,R 区间而非前缀区间, 且求的是类似于最大值的, 那么就必须用线段树了。

7.3.1.1 求 i⇐j⇐k, a[i] > a[y] && a[j] < a[k] 的数对个数

数据范围限制只能用 或 的算法求解, 这形式的问题通常都以中间点为依据来思考。

把所有三个点构成的数对以中间节点的横坐标为依据划分集合。则以 为中间点的 V 图腾, 其数量就是 1k大于k高度的点数量 * k+1n大于k高度的点数量。另一种图腾也同理。

故现在需要做的就是 查询1~k-1中 比 k 大的数量, 和 插入当前k值。区间查询和点修改。

const int N = 2e5 + 10;
typedef long long LL;
int a[N], t[N];
int n;
int great[N], low[N];
 
/* 树状数组模板 */
 
int main()
{
    cin >> n;
    for(int i = 1; i <= n; i++) cin >> a[i];
    
    for(int i = 1; i <= n; i++)
    {
        int y = a[i];
        great[i] = sum(n) - sum(y);
        low[i] = sum(y - 1);
        add(a[i], 1);
    }
    
    memset(t, 0, sizeof t);
    LL res1 = 0, res2 = 0;
    for(int i = n; i; i--)
    {
        int y = a[i];
        res1 += great[i] * (sum(n) - sum(y));
        res2 += low[i] * sum(y - 1);
        add(y, 1);
    }
    
    cout << res1 << " " << res2 << endl;
    
    return 0;
}

7.3.1.2 已知 i 头牛前面有 a[i] 头比它低, 求每头的身高

可以先手动逆序来求解: 对于样例 0 1 2 1 0, 最后一个数前面有0个比它小的, 而1到5每个数又只出现一次, 因此该数就是1。 而对于倒数第二个数前面有1个比它小的, 且此时已经确定了1的位置, 还剩下 2 3 4 5 四个数, 故这个位置上的数就是第二个数 3。 至此可以归纳出一个方法:逆序依次求解, 当前位置的数 就是剩余第 个能选数, 为 之前有几个数比 数小。

只需要实现两个操作:

  • 得到剩余第k个数
  • 删除一个数

把树状数组叶子节点都初始化为1, 删除时就将1删去。这样前缀和的含义就是剩余点的数量。

还是看样例, 继续刚才枚举的地方, 开始选倒数第三个数: 从1到5求 的值为 此时还剩下 2 4 5 三个数可以选, 而倒数第三个数前面有两个比它小的, 显然只能选最后一个 5。 多推几个可以发现, 对应的第一个 的就是该放在位置上的数。 而 组成的序列又是单调递增的, 故可以使用二分来以 的复杂度查询。

总复杂度为 树状数组嵌套二分

const int N = 1e5 + 10;
int t[N*2];
int a[N], ans[N];
int n;
 
int main()
{
    cin >> n;
    for(int i = 2; i <= n;i ++) cin >> a[i];
    
    for(int i = 1; i <= n;i ++) t[i] = lowbit(i);
    
    for(int i = n; i >= 1; i--)
    {
        int k = a[i] + 1;
        int l = -1, r = n+1;
        while(l != r - 1)
        {
            int mid = l + r >> 1;
            if(sum(mid) < k) l = mid;
            else r = mid;
        }
        ans[i] = r;
        add(r, -1);
    }
    
    for(int i = 1; i <= n; i++) cout << ans[i] << "\n";
    
    return 0;
}

7.3.1.3 找 a[i] 起往左第 k 个 a[j] > a[i] 的数 a[j]

const int N = 4e5 + 10;
int a[N];
int n, m, k;
int t[N], id[N];
int ans[N];
 
bool cmp(int &i, int &j)
{
    return a[i] > a[j];
}
 
int main()
{
    int T;
    cin >> T;
    while (T--)
    {
        n = 0;
        memset(t, 0, sizeof t);
        cin >> m >> k;
        vector<int> nums;
        for (int i = 1; i <= m; i++)
        {
            cin >> a[i];
            id[i] = i;
        }
        sort(id + 1, id + m + 1, cmp);
 
        for (int i = 1; i <= m; i++)
        {
            int x = id[i];
            if (sum(x) < k)
            {
                add(x);
                ans[x] = -1;
                continue;
            }
            int cnt = sum(x), l = -1, r = x - 1;
            while (l != r - 1)
            {
                int mid = l + r >> 1;
                if (sum(mid) < cnt - k + 1)
                    l = mid;
                else
                    r = mid;
            }
            ans[x] = a[r];
            add(x);
        }
        for (int i = 1; i <= m; i++)
            cout << ans[i] << "\n";
    }
    return 0;
}

7.3.1.4 区间合并+最大字段和

  1. 1 x y,查询区间 中的最大连续子段和,即 {}。
  2. 2 x y,把 改成 。
#define lson l, mid, u << 1
#define rson mid + 1, r, u << 1 | 1
const int N = 5e5 + 10;
int a[N];
int n,m;
 
struct Node {
    int lsum, rsum, sum, tsum;
}t[N * 4];
 
void pushup(Node &u, Node &l, Node &r)
{
    u.sum = l.sum + r.sum;
    u.lsum = max(l.lsum, l.sum + r.lsum);
    u.rsum = max(r.rsum, r.sum + l.rsum);
    u.tsum = max(max(l.tsum, r.tsum), l.rsum + r.lsum);
}
 
void pushup(int u)
{
    pushup(t[u], t[u << 1], t[u << 1 | 1]);
}
 
void build(int l , int r, int u = 1)
{
    if(l == r)
    {
        t[u] = {a[l], a[l], a[l], a[l]};
        return;
    }
    int mid = l + r >> 1;
    build(lson); build(rson);
    pushup(u);
}
 
void update(int pos, int val, int l = 1, int r = n, int u = 1)
{
    if(l == pos && r == pos)
    {
        t[u] = {val,val, val, val};
        return;
    }
    int mid = l + r >> 1;
    if(pos <= mid) update(pos, val, lson);
    else update(pos, val, rson);
    pushup(u);
}
 
Node query(int L, int R, int l = 1, int r = n, int u = 1)
{
    if(L <= l && r <= R)
        return t[u];
    int mid = l + r >> 1;
    int v = -0x3f3f3f3f, sum = 0;
    if(L > mid) return query(L,R,rson);
    else if(R <= mid) return query(L,R,lson);
    else {
        auto left = query(L, R, lson);
        auto right = query(L,R,rson);
        
        Node res;
        pushup(res, left, right);
        return res;
    }
    pushup(u);
}
 
 
int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++) cin >> a[i];
    build(1, n);
    while(m--)
    {
        int t, a, b;
        cin >> t >> a >> b;
        if(t == 1)
        {
            if(a > b) swap(a,b);
            cout << query(a, b).tsum << endl;
        }
        else
        {
            update(a, b);
        }
    }
    return 0;
}

7.3.2 区间修改+区间/单点查询

这里大部分都得用线段树, 只有特定问题可以使用树状数组, 比如可以用差分技巧解决修改某个数/某段数然后区间求和。

7.3.2.1 区间修改数值, 单点查询数值(仅限求和/异或)

第一类指令形如 C l r d,表示把数列中第 个数都加 。

第二类指令形如 Q x,表示询问数列中第 个数的值。

普通的树状数组可以实现单点修改区间查询, 这里需要做一下转化。因为树状数组最后求的是前缀和。 可以先把原数组初始化为差分数组, 即可。

int main()
{
    cin >> n >> m;
    int prev = 0;
    for(int i = 1; i <= n; i++)
    {
        int t;
        cin >> t;
        add(i, t - prev);
        prev = t;
    }
    
    while(m--)
    {
        char s[2];
        int l,r,d;
        cin >> s >> l;
        if(s[0] == 'Q')
            cout << sum(l)<< endl;
        else {
            cin >> r >> d;
            add(l, d);
            add(r + 1, -d);
        }
    }
    return 0;
}

7.3.2.2 区间修改数值, 区间查询数值(仅限求和/异或)

  1. C l r d,表示把 都加上 。
  2. Q l r,表示询问数列中第 个数的和。

列举一个区间和看看是什么样的: 每个数由若干项 的和组成, 而区间和则是把整个求和。 可以把这些数扩充为一个矩形后, 整个矩形的总和为 。 观察一下多出来的部分, 也可以用一个式子来描述: 。

故可以通过相减的方式来求出区间和。我们只需要建立两个对于差分数组a的前缀和树状数组 tr1 和 tr2, tr1中维护 的前缀和, tr2中维护 的前缀和。

输出时, 对于区间 , 为 再减去左区间 。

typedef long long LL;
const int N = 1e5 + 10;
LL tr1[N], tr2[N];
int a[N];
int n,m;
 
int lowbit(int x)
{
    return x & -x;
}
 
void add(LL tr[], int u, LL x)
{
    for(int i = u; i <= n;i += lowbit(i)) tr[i] += x;
}
 
LL sum(LL tr[], int u)
{
    LL res = 0;
    for(int i = u; i; i -= lowbit(i)) res += tr[i];
    return res;
}
 
LL prefix_sum(int u)
{
    return sum(tr1, u) * (u + 1) - sum(tr2, u);
}
 
int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n;i ++) cin >> a[i];
    for(int i = 1; i <= n;i ++)
    {
        LL b = a[i] - a[i - 1];
        add(tr1, i, b);
        add(tr2, i, (LL)b * i);
    }
    
    while(m--)
    {
        char s[2];
        int l,r,d;
        cin >> s >> l >> r;
        if(s[0] == 'Q')
            printf("%lld\n", prefix_sum(r) - prefix_sum(l - 1));
        else {
            cin >> d;
            add(tr1, l, d), add(tr2, l, d * l);
            add(tr1, r + 1, -d), add(tr2, r + 1, (r + 1) * -d);
        }
    }
    
    return 0;
}

7.3.2.3 区间修改+查询区间最大公约数(可以不用lazy)

#define lson l, mid, u << 1
#define rson mid + 1, r , u << 1 | 1
const int N = 5e5 + 10;
typedef long long ll;
struct Node {
    ll sum, gcd;
}t[N * 4];
int n,m;
ll a[N];
 
ll gcd(ll a, ll b)
{
    return b?gcd(b, a % b) : a;
}
 
void pushup(Node &u, Node &l, Node &r)
{
    u.sum = l.sum + r.sum;
    u.gcd = gcd(l.gcd, r.gcd);
}
 
void pushup(int u)
{
    pushup(t[u], t[u << 1], t[u << 1 | 1]);
}
 
void build(int l, int r, int u = 1)
{
    if(l == r)
    {
        ll b = a[r] - a[r - 1];
        t[u] = {b , b};
        return;
    }
    int mid = l + r >> 1;
    build(lson), build(rson);
    pushup(u);
}
 
void update(int pos, ll v, int l = 1, int r = n, int u = 1)
{
    if(pos == l && pos == r)
    {
        ll b = t[u].sum + v;
        t[u] = {b,b};
        return;
    }
    int mid = l + r >> 1;
    if(pos <= mid) update(pos, v, lson);
    else update(pos, v, rson);
    pushup(u);
}
 
Node query(int L, int R, int l = 1, int r = n, int u = 1)
{
    if(L <= l && r <= R)
        return t[u];
    int mid = l + r >> 1;
    if(L > mid) return query(L, R, rson);
    else if(R <= mid) return query(L,R,lson);
    else {
        auto left = query(L,R,lson);
        auto right = query(L,R,rson);
        
        Node res;
        pushup(res, left, right);
        return res;
    }
    pushup(u);
}
 
int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
        cin >> a[i];
    build(1,n);
    char op[2];
    int l,r;
    ll x;
    while(m--)
    {
        cin >> op >> l >> r;
        if(op[0] == 'Q')
        {
            auto left = query(1, l);
            auto right = query(1 + l, r);
            cout << abs(gcd(left.sum, right.gcd)) << endl;
            
        }
        else { 
            cin >> x;
            update( l, x);
            if(r + 1 <= n)
                update( r + 1, -x);
        }
    }
    return 0;
}

7.3.2.4 区间查询+区间修改 Lazy 懒标记

const int N = 1e5+ 10;
typedef long long ll;
struct Node {
    int l,r;
    ll sum, lazy;
}t[N << 2];
 
int n,m;
int a[N];
 
void pushup(int u)
{
    t[u].sum = t[u << 1].sum + t[u << 1 | 1].sum;
}
 
 
 
void pushdown(int u)
{
    Node &l = t[u << 1], &r = t[u << 1 | 1];
    if(t[u].lazy)
    {
        l.sum += (ll)t[u].lazy * (l.r - l.l + 1);
        l.lazy += t[u].lazy;
        r.sum += (ll)t[u].lazy * (r.r - r.l + 1);
        r.lazy += t[u].lazy;
        t[u].lazy = 0;
    }
}
 
void build(int l, int r, int u = 1)
{
    t[u] = {l,r,0,0};
    if(l == r)
    {
        t[u] = {l,r,a[l], 0};
        return;
    }
    int mid = l + r >> 1;
    build(lson), build(rson);
    pushup(u);
}
 
void update(int L, int R, int val, int l = 1, int r = n, int u = 1)
{
    if(L <= l && r <= R)
    {
        t[u].sum += (ll)val * (t[u].r - t[u].l + 1);
        t[u].lazy += val;
        return;
    }
    pushdown(u);
    int mid = l + r >> 1;
    if(L <= mid) update(L,R,val,lson);
    if(R > mid) update(L,R,val,rson);
    pushup(u);
}
 
ll query(int L, int R, int l = 1, int r= n, int u = 1)
{
    if(L <= l && r <= R)
        return t[u].sum;
    pushdown(u);
    int mid =l + r >> 1;
    ll sum = 0;
    if(L <= mid) sum += query(L, R, lson);
    if(R > mid) sum += query(L,R, rson);
    pushup(u);
    return sum;
}
 
int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
        cin >> a[i];
    build(1,n);
    char op[2];
    int l,r,x;
    while(m--)
    {
        cin >> op >> l >> r;
        if(op[0] == 'Q')
        {
            cout << query(l,r) << endl;
        }
        else {
            cin >> x;
            update(l,r,x);
        }
    }
    return 0;
}

7.3.2.5 区间查询+区间修改+存线段+扫描线+离散化存储

给几个矩形两个角的坐标, 求所有矩形并集的总面积。

const int N = 1e4 + 10;
struct Seg {
    double x, y1, y2;
    int cnt;
    bool operator < (const Seg &w) const {
        return x < w.x;
    }
}seg[N*2];
int n,m, cnt;
struct Node {
    int l, r;
    double len;
    int cnt;
}t[N*8];
vector<double> ys;
int find(double x)
{
    return lower_bound(ys.begin(), ys.end(), x) - ys.begin();
}
 
void pushup(int u)
{
    if(t[u].cnt)
        t[u].len = ys[t[u].r + 1] - ys[t[u].l];
    else if(t[u].r != t[u].l)
        t[u].len = t[u << 1].len + t[u << 1 | 1].len;
    else
        t[u].len = 0;
}
 
void build(int l ,int r ,int u = 1)
{
    t[u] = {l,r, 0,0};
    if(l != r)
    {
        int mid = l + r >> 1;
        build(l, mid, u << 1);
        build(mid + 1, r, u << 1 | 1);
    }
    
}
 
void update(int L, int R, int val, int u = 1)
{
    if(L <= t[u].l && t[u].r <= R)
    {
        t[u].cnt += val;
        pushup(u);
        return;
    }
    int mid = t[u].l + t[u].r >> 1;
    if(L <= mid) update(L, R, val, u << 1);
    if(R > mid) update(L, R, val, u << 1 | 1);
    pushup(u);
}
 
int main()
{
    int T = 1;
    while(cin >> n,n)
    {
        memset(t, 0, sizeof t);
        ys.clear();
        cnt = 0;
        for(int i = 1; i <= n;i ++)
        {
            double x1, y1, x2, y2;
            cin >> x1 >> y1 >> x2 >> y2;
            seg[cnt++] = {x1, y1, y2, 1};
            seg[cnt++] = {x2, y1, y2, -1};
            ys.push_back(y1), ys.push_back(y2);
        }
    
        sort(seg, seg + cnt);
        
        sort(ys.begin(), ys.end());
        ys.erase(unique(ys.begin(), ys.end()), ys.end());
        build(0,ys.size() - 2, 1);
        
        double res = 0;
        
        for(int i = 0; i < cnt; i++)
        {
            if(i) res += t[1].len * (seg[i].x - seg[i - 1].x);
            update(find(seg[i].y1), find(seg[i].y2) - 1, seg[i].cnt);
        }
        printf("Test case #%d\n", T++);
        printf("Total explored area: %.2lf\n\n", res);
    }
    return 0;
}

7.4 Treap 平衡树

STL 的 set 和 map 就是 Treap 的变种, 实质就是 Birnary Search Tree(BST) + Heap。

  1. 插入数值 。
  2. 删除数值 (若有多个相同的数,应只删除一个)。
  3. 查询数值 的排名(若有多个相同的数,应输出最小的排名)。
  4. 查询排名为 的数值。
  5. 求数值 的前驱(前驱定义为小于 的最大的数)。
  6. 求数值 的后继(后继定义为大于 的最小的数)。
using namespace std;
const int N = 1e5 + 10, INF = 0x3f3f3f3f;
int idx;
int root;
struct Node{
    int l, r;
    int key, val;
    int size, cnt;
}tr[N];
int n;
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 build()
{
    get_node(-INF), get_node(INF);
    root = 1, tr[root].r = 2;
    pushup(root);
}
 
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 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);
}
 
int remove(int &p, int key)
{
    if(!p) return 0;
    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_val(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_val(tr[p].l, key);
    return tr[tr[p].l].size + tr[p].cnt + get_rank_by_val(tr[p].r, key);
}
 
int get_val_by_rank(int p, int size)
{
    if(!p) return INF;
    else if(tr[tr[p].l].size >= size) return get_val_by_rank(tr[p].l, size);
    else if(tr[tr[p].l].size + tr[p].cnt >= size) return tr[p].key;
    return get_val_by_rank(tr[p].r, size - tr[tr[p].l].size - tr[p].cnt);
}
 
int get_prev(int &p, int 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)
{
    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()
{
    cin >> n;
    while(n--)
    {
        int op, x;
        cin >> op >> x;
        if(op == 1) insert(root, x);
        else if(op == 2) remove(root, x);
        else if(op == 3) cout << get_rank_by_val(root, x) << endl;
        else if(op == 4) cout << get_val_by_rank(root, x) << endl;
        else if(op == 5) cout << get_prev(root, x) << endl;
        else cout << get_next(root, x) << endl;
    }
    return 0;
}

7.5 可持久化数据结构

7.5.1 Tire树 最大异或和

  1. A x:添加操作,表示在序列末尾添加一个数 ,序列的长度 增大 。
  2. Q l r x:询问操作,你需要找到一个位置 ,满足 ,使得: 最大,输出这个最大值。
const int N = 6e5, M = 24 * N;
int root[M];
int tr[M][2], maxid[M];
int s[N], idx;
int n,m;
 
void insert(int u, int k, int p, int q)
{
    if(k < 0)
    {
        maxid[q] = u;
        return;
    }
    int v = s[u] >> k & 1;
    if(p) tr[q][v ^ 1] = tr[p][v ^ 1];
    tr[q][v] = ++idx;
    insert(u, k - 1, tr[p][v], tr[q][v]);
    maxid[q] = max(maxid[tr[q][v]], maxid[tr[q][v ^ 1]]);
}
 
int query(int r, int C , int l)
{
    int p = r;
    for(int i = 23; i >= 0; i--)
    {
        int v = C >> i & 1;
        if(maxid[tr[p][v ^ 1]] >= l) p = tr[p][v ^ 1];
        else p = tr[p][v];
    }
    
    return s[maxid[p]] ^ C;
}
 
int main()
{
    cin >> n >> m;
    
    maxid[0] = -1;
    root[0] = ++idx;
    insert(0, 23, 0, root[0]);
    
    for(int i = 1; i <= n; i++)
    {
        cin >> s[i];
        root[i] = ++idx;
        s[i] ^= s[i - 1];
        insert(i, 23, root[i - 1], root[i]);
    }
    
    char op[2];
    int l, r, x;
    while(m--)
    {
        cin >> op;
        if(op[0] == 'A')
        {
            cin >> x;
            ++n;
            s[n] = s[n - 1] ^ x;
            root[n] = ++idx;
            insert(n, 23, root[n - 1], root[n]);
        }
        else {
            cin >> l >> r >> x;
            cout << query(root[r - 1], x ^ s[n], l-1) << endl;
        }
    }
    
    return 0;
}

7.5.2 主席树/持久化线段树 第K小数

给定长度为 的整数序列 ,下标为 。

现在要执行 次操作,其中第 次操作为给出三个整数 ,求 (即 的下标区间 )中第 小的数是多少。

const int N = 1e5 + 10, M = 1e4 + 10;
int a[N];
struct Node {
    int l, r;
    int cnt;
}tr[N * 4 + N * 17];
int root[N], idx;
vector<int> nums;
int n,m;
 
int find(int x)
{
    return lower_bound(nums.begin(), nums.end(), x) - nums.begin();
}
 
int build(int l, int r)
{
    int p = ++idx;
    if(l == r) return p;
    int mid = l + r >> 1;
    tr[p].l = build(l, mid);
    tr[p].r = build(mid + 1, r);
    return p;
}
 
int insert(int p, int l, int r, int x)
{
    int q = ++idx;
    tr[q] = tr[p];
    if(l == r) 
    {
        tr[q].cnt ++;
        return q;
    }
    int mid = l + r >> 1;
    if(x <= mid) tr[q].l = insert(tr[p].l, l, mid, x);
    else tr[q].r = insert(tr[p].r, mid + 1, r, x);
    tr[q].cnt = tr[tr[q].l].cnt + tr[tr[q].r].cnt;
    return q;
}
 
int query(int q, int p, int l, int r, int k)
{
    if(l == r) return r;
    int cnt = tr[tr[q].l].cnt - tr[tr[p].l].cnt;
    int mid = l + r >> 1;
    if(k <= cnt) return query(tr[q].l, tr[p].l, l, mid, k);
    else return query(tr[q].r, tr[p].r, mid + 1, r, k - cnt);
}
 
int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n;i ++) 
    {
        cin >> a[i];
        nums.push_back(a[i]);
    }
    
    sort(nums.begin(), nums.end());
    nums.erase(unique(nums.begin(),nums.end()), nums.end());
    
    root[0] = build(0, nums.size() - 1);
    
    for(int i = 1; i <= n; i++)
        root[i] = insert(root[i - 1], 0, nums.size() - 1, find(a[i]));
    
    while(m--)
    {
        int l, r, k;
        cin >> l >> r >> k;
        cout << nums[query(root[r], root[l - 1], 0, nums.size() - 1, k)] << endl;
    }
    
    return 0;
}