算法模版自用(杂)

文章目录

  • 算法
    • 库函数
      • next_permutation(start,end) prev_permutation(start,end) (全排列函数)
      • nth_element (求第k小值)
      • next(it,num),prev(it,num)
      • min_element(begin(),end()),max_element(begiin(),end()) (取最小值最大值)
    • _int128的输入输出
    • STL
      • list
    • 数据结构
      • 单调栈
      • 单调队列
      • 对顶堆
      • 链表
        • 单链表
        • 双向链表
        • y总模版
      • 树状数组
      • 线段树
      • 可持久化线段树
    • 字符串
      • KMP
      • 最长回文串
    • 图论
      • 最短路
      • 前置知识
      • 多源最短路
        • **Floyd 算法**(n^3^)
        • Floyd 算法的扩展
      • 最长路
      • 次短路及k短路
        • 次短路
        • k短路(Astar)(knlogn)
      • 分层图
      • 最小生成树
        • 1.prime(朴素版)
        • 2.kruskal
        • 3.次小生成树(倍增)
          • 复杂度
          • 代码
      • 最近公共祖先(lca)
        • 1.朴素算法
          • 过程
          • 性质
        • 2.倍增算法
          • 复杂度
          • 代码
        • 3.应用
      • 连通性相关
        • 强联通分量
          • 简介
          • targan算法
            • 时间复杂度
            • 代码
            • 缩点
          • 最大半联通子图
            • 问题分析
            • 代码
        • 双联通分量
          • 前置概念
          • 边联通分量
            • 解法
            • 代码
            • 例题
          • 点联通分量
            • 割点的判断
      • 二分图
        • 匈牙利算法(二分图的最大匹配)
          • 时间复杂度
          • 代码
    • 基础算法
      • 快速排序
      • 归并排序
      • 贪心
      • 二分
      • 取整
      • 卡常

算法

库函数

next_permutation(start,end) prev_permutation(start,end) (全排列函数)

[库函数介绍](C++中全排列函数next_permutation 用法-CSDN博客)

[库函数原理](C++中next_permutation函数的使用方法、原理及手动实现_c++ next_permutation-CSDN博客)

库函数原理基本解释:
因为在一个全排列中全为降序是最大的,此时就需要找到前面的一个数(即第一个小于的数),然后进行上述过程,而在新开的排列中,要把后面的序列仍未降序,翻转一下即可
[洛谷P1706 全排列问题](P1706 全排列问题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn))

//模拟next_permutation()(贪心)
#include<iostream>
using namespace std;
const int N=10;
int n;
int a[N];bool next_permutation(){int k=n-1;while(k&&a[k-1]<a[k]) k--;//从右往左找第一个不为降序的if(!k) return false;//没有找到,序列已经为最大int t=k;while(t<n&&a[t]>a[k-1]) t++;//找到降序序列中大于a[k-1]的最小的一个t--;swap(a[k-1],a[t]);reverse(a+k,a+n);return true;
}int main(){cin>>n;for(int i=0;i<n;i++) a[i]=i+1;do{for(int i=0;i<n;i++) cout<<"    "<a[i];}while(next_permutation());reutrn 0;
}

nth_element (求第k小值)

stl,o(n),常数较大
[具体用法](STL 之 nth_element详解-CSDN博客)

next(it,num),prev(it,num)

next(it,num),迭代器it的后num的迭代器,prev则相反(num可为负数)

min_element(begin(),end()),max_element(begiin(),end()) (取最小值最大值)

返回的是指针

_int128的输入输出

由于c++的输入输出不支持_int128,以下是_int128的输入输出板子

#include <bits/stdc++.h>
using namespace std;
inline __int128 read(){__int128 x = 0, f = 1;char ch = getchar();while(ch < '0' || ch > '9'){if(ch == '-')f = -1;ch = getchar();}while(ch >= '0' && ch <= '9'){x = x * 10 + ch - '0';ch = getchar();}return x * f;
}
inline void print(__int128 x){if(x < 0){putchar('-');x = -x;}if(x > 9)print(x / 10);putchar(x % 10 + '0');
}
int main(void){__int128 a = read();__int128 b = read();print(a + b);cout << endl;return 0;
}

STL

list

[基本用法](C++ STL标准库: std::list使用介绍、用法详解-CSDN博客)

sort([cmp]),unique

数据结构

单调栈

常见模型:找出每个数左边离它最近的比它大/小的数

int tt = 0;
for (int i = 1; i <= n; i ++ )
{while (tt && check(stk[tt], i)) tt -- ;stk[ ++ tt] = i;
}

单调队列

常见模型:找出滑动窗口中的最大值/最小值

int hh = 0, tt = -1;
for (int i = 0; i < n; i ++ )
{while (hh <= tt && check_out(q[hh])) hh ++ ;  // 判断队头是否滑出窗口while (hh <= tt && check(q[tt], i)) tt -- ;q[ ++ tt] = i;
}

对顶堆

动态维护一个序列上第 k k k大的数, k k k的值可能会发生变化。

对于这类问题,可以用对顶堆来解决(避免写权值线段树的繁琐)

对顶堆由一个大根堆和一个小根堆组成,小跟堆维护大值即前 k k k大的值,大跟堆维护小值即比第 k k k大数小的其它数

支持以下操作:

  • 维护:当小根堆的大小小于 k k k时,不断将大根堆堆顶元素取出并插入小根堆,直到小根堆的大小等于 k k k;当小根堆的大小大于 k k k时,不断将小根堆堆顶元素取出并插入大根堆,直到小根堆的大小等于 k k k
  • 插入元素:若插入的元素大于等于小根堆堆顶元素,则将其插入小根堆,否则将其插入大根堆,然后维护对顶堆;
  • 查询第 k k k大元素:小根堆堆顶元素即为所求;
  • k k k + 1 / − 1 +1/-1 +1/1:根据新的 k k k值维护对顶堆;

链表

单链表
// head存储链表头,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪个节点
int r[N], val[N], pos[N], idx = 1;//r存其右面的数,val存值,pos存元素地址val[idx] = x, r[idx] = r[0], r[0] = idx++;//头插法val[idx] = x, r[idx - 1] = idx++;//顺序插入r[x] = r[r[x]]; //将x右边的元素删除val[idx] = y, r[idx] = r[pos[x]], r[pos[x]] = idx++;//将元素y插入到元素x右面
双向链表
int r[N], l[N], val[N], pox[N], idx = 1;r[idx - 1] = idx, l[idx] = idx - 1, idx++;//顺序插入pre = pox[x], val[idx] = y, pox[y] = idx, r[idx] = r[pre], l[idx] = pre, l[r[pre]] = idx, r[pre] = idx++;//将元素y插入到x右面p = pos[x], r[l[p]] = r[p], l[r[p]] = l[p];//将元素x删除for (int i = r[1]; i; i = r[i]) cout << val[i] << " ";//遍历元素
//尾结点和头结点都是0
y总模版
// head存储链表头,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪个节点
int head, e[N], ne[N], idx;// 初始化
void init()
{head = -1;idx = 0;
}// 在链表头插入一个数a
void insert(int a)
{e[idx] = a, ne[idx] = head, head = idx ++ ;
}// 将头结点删除,需要保证头结点存在
void remove()
{head = ne[head];
}
// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点
int e[N], l[N], r[N], idx;// 初始化
void init()
{//0是左端点,1是右端点r[0] = 1, l[1] = 0;idx = 2;
}// 在节点a的右边插入一个数x
void insert(int a, int x)
{e[idx] = x;l[idx] = a, r[idx] = r[a];l[r[a]] = idx, r[a] = idx ++ ;
}// 删除节点a
void remove(int a)
{l[r[a]] = l[a];r[l[a]] = r[a];
}

树状数组

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

1.性质

1.x的父节点是x+lowbit(x)
2.每个x代表的区间长度为lowbit(x),[x-lowbit(x)+1,x]
3.每个tri[x]由b[x],tri[x-20],tri[x-21]…(2k<lowbit(x),xk+1>=lowbit(x+1))


2.树状数组的o(N) 建树方法
由于每个tr[i]都是以i为结尾,**长度为lowbit(i)**的区间的和,故可预处理前缀和进行o(N)建树

int sums[N],tri[N];//sums是前缀和
for(int i=1;i<=n;i++) tri[i]=sums[i]-sums[i-lowbit(i)];

3.树状数组维护区间最值

void update(int x, int c) {//(logn)^2 单点修改b[x] = c;int lx;while (x <= n) {tri[x] = b[x];lx = lowbit(x);for (int i = 1; i < lx; i <<= 1)tri[x] = max(tri[x], tri[x - i]);}
}int query(int l, int r) {//(logn)^2 区间查询int ans = 0;while (l <= r) {ans = max(ans, b[r]);for (r--; r - lowbit(r) >= l; r -= lowbit(r))//这样的写法不会死循环ans = max(tri[r], ans);}return ans;
}

线段树

线段树是一颗二叉树,节点数为2n-1,可以以O(log n)的操作进行单点修改,区间修改,区间查询(区间修改和区间查询大概为4log n,常数比树状数组大,懒标记由于此性质就可优化为O(log n))

线段树维护的是区间,即是线段,而不是点,要注意

注意事项 :要开4N的空间(因此有时候要离散化以压缩空间),证明如下 csdn证明

1.单点修改,区间查询

题目链接[P3374 【模板】树状数组 1](P3374 [模板]树状数组 1 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn))

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<cstdio>
using namespace std;
const int N = 5e5 + 10;
typedef long long ll;int w[N];
int n, m;
struct Node {int l, r;ll sum;
}tr[N*4];void pushup(int u) {tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}void build(int u, int l, int r) {if (l == r) tr[u] = { l,r,w[l] };else {tr[u] = { l,r };int mid = l + r >> 1;build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);pushup(u);}
}void modify(int u, int x, int d) {//更新if (tr[u].l == x && tr[u].r == x) tr[u].sum += d;else {int mid = tr[u].l + tr[u].r >> 1;if (x <= mid) modify(u << 1, x, d);else modify(u << 1 | 1, x, d);pushup(u);}
}ll query(int u, int l, int r) {、、查询if (tr[u].l >= l && tr[u].r <= r) return tr[u].sum;else {int mid = tr[u].l + tr[u].r >> 1;ll res = 0;if (l <= mid) res = query(u << 1, l, r);if (r > mid) res += query(u << 1 | 1, l, r);return res;}
}int main() {scanf("%d%d", &n,&m);for (int i = 1; i <= n; i++) cin >> w[i];build(1, 1, n);while (m--) {int op, x, y;scanf("%d%d%d", &op, &x, &y);if (op == 1) modify(1, x, y);else printf("%d\n", query(1, x, y));}return 0;}

2.区间修改,区间查询(涉及懒标记,即add)

加上懒标记的节点sum会维护上,但其子节点的sum和add不会维护上,当查询或修改涉及一个节点是,需要其父节点进行pushdown操作,这样就可以维护该节点的sum和add了

题目链接[P3372 【模板】线段树 1](P3372 [模板]线段树 1 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn))

#include<iostream>
#include<cstdio>
using namespace std;
const int N = 1e5 + 10;
typedef long long ll;int w[N], n, m;
struct Node {int l, r;ll sum, add;//add即为懒标记
}tr[N * 4];inline void pushup(int u) {tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}inline void pushdown(int u) {auto& root = tr[u], &right = tr[u << 1 | 1], &left = tr[u << 1];if (root.add) {left.add += root.add, left.sum += (ll)(left.r - left.l + 1) * root.add;right.add += root.add, right.sum += (ll)(right.r - right.l + 1) * root.add;root.add = 0;}
}void build(int u, int l, int r) {if (l == r) tr[u] = { l,r,w[l],0 };else {tr[u] = { l,r };int mid = l + r >> 1;build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);pushup(u);}
}void modify(int u, int l, int r, int d) {if (tr[u].l >= l && tr[u].r <= r) {   //由于只要该区间属于要修改的区间就立即返回(并加上懒标记),所以modify可做到log ntr[u].sum += (ll)(tr[u].r - tr[u].l + 1) * d;tr[u].add += d;}else {pushdown(u);int mid = tr[u].l + tr[u].r >> 1;if (l <= mid) modify(u << 1, l, r, d);if (r > mid) modify(u << 1 | 1, l, r, d);pushup(u);}
}ll query(int u, int l, int r) {if (tr[u].l >= l && tr[u].r <= r) return tr[u].sum;else {pushdown(u);ll res = 0;int mid = tr[u].l + tr[u].r >> 1;if (l <= mid) res += query(u << 1, l, r);if (r > mid) res += query(u << 1 | 1, l, r);return res;}
}int main() {scanf("%d%d", &n, &m);for (int i = 1; i <= n; i++) scanf("%d", w + i);build(1, 1, n);while (m--) {int op, x, y, k = 0;cin >> op >> x >> y;if (op == 1) {scanf("%d", &k);modify(1, x, y, k);}else printf("%lld\n", query(1, x, y));}return 0;
}

3.扫描线法(用于解决多个矩形的面积(可能相交)),[详解见]一文读懂扫描线算法 - 知乎 (zhihu.com))
题目链接[acwing247. 亚特兰蒂斯](247. 亚特兰蒂斯 - AcWing题库)

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<vector>
using namespace std;
const int N = 10010;struct Segment {double x, y1, y2;int cnt;bool operator<(const Segment& t) const {return x < t.x;}
}seg[2 * N];;
vector<double> ys;struct Node {int l, r, cnt;double len;
}tr[8 * N];
int n;inline void pushup(int u) {if (tr[u].cnt) tr[u].len = ys[tr[u].r + 1] - ys[tr[u].l];else if (tr[u].l != tr[u].r) {tr[u].len = tr[u << 1].len + tr[u << 1 | 1].len;}else tr[u].len = 0;
}inline int find(double x) {return lower_bound(ys.begin(), ys.end(), x) - ys.begin();
}void build(int u, int l, int r) {tr[u] = { l,r,0,0 };if (l != r) {int mid = l + r >> 1;build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);}
}void modify(int u, int l, int r, int k) {if (tr[u].l >= l && tr[u].r <= r) {tr[u].cnt += k;pushup(u);}else {int mid = tr[u].l + tr[u].r >> 1;if (l <= mid) modify(u << 1, l, r, k);if (r > mid) modify(u << 1 | 1, l, r, k);pushup(u);}
}int main() {int T = 1;while (scanf("%d", &n), n) {ys.clear();for (int i = 0, j = 0; i < n; i++) {double x1, x2, y1, y2;scanf("%lf%lf%lf%lf", &x1, &y1, &x2, &y2);seg[j++] = { x1,y1,y2,1 }, seg[j++] = { x2,y1,y2,-1 };ys.push_back(y1), ys.push_back(y2);}sort(ys.begin(), ys.end());ys.erase(unique(ys.begin(), ys.end()), ys.end());build(1, 0, ys.size() - 2);sort(seg, seg + 2 * n);double res = 0;for (int i = 0; i < 2 * n; i++) {if (i) res += tr[1].len * (seg[i].x - seg[i - 1].x);modify(1, find(seg[i].y1), find(seg[i].y2) - 1, seg[i].cnt);}printf("Test case #%d\n", T++);printf("Total explored area: %.2f\n\n", res);}return 0;
}

[可解决的问题的一些板子](线段树(Segment Tree)(上) - 知乎 (zhihu.com))

可持久化线段树

[较详细介绍](算法学习笔记(50): 可持久化线段树 - 知乎 (zhihu.com))

[acwing 255.第k小数](255. 第K小数 - AcWing题库)(非严格第k小数)

将数值大小作为线段,用线段树维护数值数目,假设求0到r区间的第k小数,先求0到Mid(Mid=(0+r)/2)的数值数目cnt,若k<cnt,则递归到区间0到Mid继续求解,反之则递归到Mid到r。若想求l到r区间的第k小数,则先求0到r中的数值数目与0到l-1的数值数目之差,下续操作与上面相同,维护历史版本则需要可持久化线段树

//这题是静态主席树(不涉及点的修改)
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;const int N = 100010, M = 10010;int n, m;
int a[N];
vector<int> nums;struct Node {int l, r;//表示左右儿子的地址int cnt;
}tr[N * 4 + N * 17];//每次修改需要log(n)的空间,n次修改需要nlog(n)int root[N], idx;//每次版本的根节点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 u, int l, int r, int x) {int p = ++idx;tr[p] = tr[u];//先连接上各版本的节点if (l == r) {tr[p].cnt++;return p;}int mid = l + r >> 1;if (x <= mid) tr[p].l = insert(tr[p].l, l, mid, x);//x在左边,左边节点改变else tr[p].r = insert(tr[p].r, mid + 1, r, x);tr[p].cnt = tr[tr[p].l].cnt + tr[tr[p].r].cnt;//更新父节点return p;
}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.emplace_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;}}

字符串

KMP

时间复杂度通常为o(n+m),最坏为o(2n)

代码模版

#include<iostream>
using namespace std;
const int N = 1e6 + 10;
int nex[N];
string s1, s2;int main() {cin >> s1 >> s2;s1 = " " + s1, s2 = " " + s2;for (int i = 2, j = 0; i < s2.size(); i++) {while (j && s2[i] != s2[j + 1]) j = nex[j];if (s2[i] == s2[j + 1]) j++;nex[i] = j;}for (int i = 1, j = 0; i < s1.size(); i++) {while (j && s1[i] != s2[j + 1]) j = nex[j];if (s1[i] == s2[j + 1]) j++;if (j == s2.size() - 1) {cout << i - s2.size() + 2 << endl;j = nex[j];//进行下一次匹配}}for (int i = 1; i < s2.size(); i++) cout << nex[i] << " ";return 0;
}

给你一个字符串 s1,它是由某个字符串 s2 不断自我连接形成的(保证至少重复 2 次)。但是字符串 s2 是不确定的,现在只想知道它的最短长度是多少。

答:n-next[n]

最长回文串


1.二分加字符串哈希

下面计算了字符串的回文串个数,稍加修改即可计算最长回文串

时间复杂度:O(nlogn)

#include<iostream>
#include<cstring>
using namespace std;
const int N = 1010, P = 131;#define endl '\n'using ull = unsigned long long;int n, ans;
ull h[N], hask1[N], hask2[N];
char s[N];ull gethask1(int l, int r) {return hask1[r] - hask1[l - 1] * h[r - l + 1];
}ull gethask2(int l, int r) {return hask2[l] - hask2[r + 1] * h[r - l + 1];
}int query1(int x) {//奇数长度int l = 0, r = min(x, n - x);//二分回文字符串长度while (l < r) {int mid = l + r + 1 >> 1;if (gethask1(x - mid, x) == gethask2(x, x + mid)) l = mid;else r = mid - 1;}return r;
}int query2(int x) {//偶数长度int l = 0, r = min(x, n - x);while (l < r) {int mid = l + r + 1>> 1;if (gethask1(x - mid + 1, x) == gethask2(x + 1, x + mid)) l = mid;else r = mid - 1;}return r;
}int main() {cin >> s + 1;n = strlen(s + 1);h[0] = 1;for (int i = 1; i <= n; i++) {hask1[i] = hask1[i - 1] * P + s[i];h[i] = h[i - 1] * P;}for (int i = n; i >= 1; i--) hask2[i] = hask2[i + 1] * P + s[i];for (int i = 1; i <= n; i++)ans += query1(i) + query2(i);cout << ans + n << endl;//序列中每个单独的字符也是一个回文串,所以要加上nreturn 0;}

对上面的代码去除二分,再在每个字符之间加上#(类似manacher算法),可在O(n)的时间内求出最长回文串

unsigned long long order1[2000100], order2[2000100];
unsigned long long pwd[2000100], base = 13331;
char s[2000100];
char s2[2000100];
int ct = 1;unsigned long long gethash1(int l, int r)
{return order1[r] - order1[l - 1] * pwd[r - l + 1];
}
unsigned long long gethash2(int l, int r)
{return order2[l] - order2[r + 1] * pwd[r - l + 1];
}
int main()
{pwd[0] = 1;for (int i = 1; i <= 1000000; i++)pwd[i] = pwd[i - 1] * base;int CASE = 1;while (~scanf("%s", s)){if (!strcmp(s, "END"))break;printf("Case %d: ", CASE++);int len = strlen(s);ct = 1;s2[ct++] = '#';for (int i = 0; i < len; i++){s2[ct++] = s[i];s2[ct++] = '#';}len = ct - 1;order1[0] = order2[len + 1] = 0;for (int i = 1; i <= len; i++) //正哈希order1[i] = order1[i - 1] * base + s2[i];for (int i = len; i >= 1; i--) //逆哈希order2[i] = order2[i + 1] * base + s2[i];int maxx = 0;for (int i = 1; i <= len; i++) //On求最长回文子串while (i - maxx >= 1 && i + maxx <= len &&gethash1(i - maxx, i + maxx) == gethash2(i - maxx, i + maxx))maxx++;printf("%d\n", maxx - 1);}return 0;
}

图论

最短路

前置知识

求最短路时,往往分为四种类型

  • bfs:边的权值都相同
  • 双端队列bfs:边的权值只有 0 0 0 x x x两种
  • dp:拓扑图
  • 以下知识:有环皆权值不符合上面

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传


1.堆优化dijkstra

const int N = 1e4 + 10, M = 5e5 + 10;using pii = pair<int, int>;
using ll = long long;int h[N], e[M], ne[M], idx = 1, w[M];
priority_queue<pii, vector<pii>, greater<pii>> q;
int n, m, s;
ll dist[N];
bool st[N];inline void add(int a, int b, int c) {e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}void dijkstra() {for (int i = 1; i <= n; i++) dist[i] = (1ll << 31) - 1;dist[s] = 0;q.push({ dist[s],s });while (q.size()) {auto t = q.top();q.pop();ll distance = t.first, ver = t.second;if (st[ver]) continue;//冗余st[ver] = true;for (int i = h[ver]; i; i = ne[i]) {int j = e[i];if (dist[j] > distance + w[i]) {dist[j] = distance + w[i];q.push({ dist[j],j });}}}
}

多到一最短路:反向建边用dijkstra

[最短路,最长路,次短路](最短路 || 最长路 || 次短路 - Poetic_Rain - 博客园 (cnblogs.com))


多源最短路

Floyd 算法(n3)

是用来求任意两个结点之间的最短路的。

适用于任何图,不管有向无向,边权正负,但是最短路必须存在。(不能有负环)

具体描述见oiwiki

代码如下

  memset(g,0x3f,sizeof g);
for(int i=1;i<=n;i++) g[i][i]=0;for(int k=1;k<=n;k++)//必须在最外层for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)g[i][j]=min(g[i][j],g[i][k]+g[k][j]);
Floyd 算法的扩展

1.传递闭包

给定n个元素,m个传递关系(边),判断两个点之间的传递关系(或是否矛盾)

  • 题1:[传递闭包模版题](B3611 [模板]传递闭包 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn))

思路:判断两点是否联通,做一遍稍微修改的floyd就ok(其实用dfs,o(n2)就能做到)

for(int k=1;k<=n;k++)//必须在最外层for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)g[i][j]|=g[i][k]&g[k][j];

该题用bitset还有o(n2)的做法,思想是状态压缩,i能到k,则k能到的点i都能到

bitset<n> g[n];
for(int k=1;k<=n;k++) //最外层for(int i=1;i<=n;i++)if(g[i][k]) g[i]|=g[k];
  • 题2:给定n个点,m个关系(a>b),判断是否矛盾,如果不矛盾,则判断是否能确定每个点的大小关系

    思路:跑一遍floyd,如果存在g[a][a] = 1,则存在矛盾(a不可能大于它本身),如果存在g[a][b] = g[b][a] = 0,则为无法确定,反之则为可确定

2.最小环

大概形容:假设有一个最小环,它里面的点肯定有一个最大编号k,此时枚举i,j(i,j != k),g[i][k]和g[k][j]是已经确定的,g[i][j]取最小即为不包括点k的最小值,枚举到最小的g[i][k]+g[k][j]+g[i][k]就是这个最小环(i,j可能会重复,最小环节点数目大于等于2)
那么就可以将集合划分为k个,每个的定义为 里面
点的最大编号为i(0 < i <=k)
,那么这个集合中肯定包含上面提到的最小环,当floyd运行到第m阶段开始时,此时g[i][j]为第m-1时的大小,直接枚举i,j,然后计算最小环

	for (int k = 1; k <= n; k++) {for (int i = 1; i < k; i++)for (int j = i + 1; j < k; j++) {//若最小环不小于三个节点,i!=j即可if (ll(d[i][j]) + a[i][k] + a[k][j] < ans) {//注意这边是aans = d[i][j] + a[i][k] + a[k][j];}}for (int i = 1; i <= n; i++)for (int j = 1; j <= n; j++) if (d[i][j] > d[i][k] + d[k][j]) {d[i][j] = d[i][k] + d[k][j];pos[i][j] = k;//计算路径,可据此输出最小环(递归)}}

3.floyd的增量问题

  • 问题背景:如果已经跑了一遍floyd,此时修改了某条边,此时计算新的多源最短路

  • 思路:直接去想是再跑一边floyd,但会复杂度角高,因为这条边被修改,所以只有走了这条边,两点之间的最短路才有可能改变,故只需枚举i,j,更新即可,o(n2)

  • 例题:[传送门]([P6464 传智杯 #2 决赛] 传送门 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn))

#include<bits/stdc++.h>
const int INF = 0x3f3f3f3f;
using namespace std;
const int maxn = 1e105;
int dp[maxn][maxn];//抄袭有奖qaq
int main(){int n, m; scanf("%d %d", &n, &m);for(int i = 1; i <= n; i ++)for(int j = 1; j <= n; j ++)if(i == j) dp[i][j] = 0;else dp[i][j] = INF;int u, v, w;for(int i = 0; i < m; i ++){scanf("%d %d %d", &u, &v, &w);//建立双向边dp[u][v] = w;dp[v][u] = w;}//floyd 算法for(int k = 1; k <= n; k ++)for(int i = 1; i <= n; i ++)for(int j = 1; j <= n; j ++)dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j]);//假设假设建立传送门的点对为(i,j)(默认j > i),枚举减少的距离任意一个点对点(a, b)(这里默认b > a)距离为int ans = INF, res;for(int i = 1; i < n; i ++)for(int j = i + 1; j <= n; j ++){res = 0;for(int a = 1; a < n; a ++)for(int b = a + 1; b <= n; b ++)if(a != i || b != j)res += min(dp[a][b], min(dp[a][i] + dp[j][b], dp[a][j] + dp[i][b]));//这道题更新的边为零,故省略了dp[i][j]和dp[j][i]  ans = min(ans, res);}printf("%d\n", ans);return 0;
}

4.有限制的floyd

先来看一道例题[牛站](345. 牛站 - AcWing题库)

题意如下:给定一张由 T 条边构成的无向图,点的编号为 1~1000之间的整数,求从起点 S 到终点 E恰好经过 N 条边(可以重复经过)的最短路。

  • 前情提要:普通的floyd似乎对这类题无法下手,但有限制的floyd便可以解决这类问题

先来看什么是有限制的floyd

	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]);//a代表步数为k1,b代表步数为k2,则temp代表步数为k1+k2memcpy(c, temp, sizeof temp);

我们发现做一次类Floyd好像不用并不会去用该次的结果(a,b数组存储上一次的结果)自我更新,只会用上一次的结果来更新一次,不像Floyd那样可以自己更新自己多

有限制的floyd具有结合律,故temp代表的步数就是a与b的步数之和,又因为具有结合律,所以可用快速幂的思想将o(Nn3)优化为o(logNN3)

#include<iostream>
#include<cstring>
#include<map>
using namespace std;const int N = 210;
int res[N][N], g[N][N];//g刚开始代表经过一条边
int k, n, m, S, E;
map<int, int> id;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]);//a代表步数为k1,b代表步数为k2,则temp代表步数为k1+k2memcpy(c, temp, sizeof temp);
}void qmi() {//符合结合律,可使用类似快速幂的思想memset(res, 0x3f, sizeof res);for (int i = 1; i <= n; i++) res[i][i] = 0;//代表经过零条边while (k) {if (k & 1) mul(res, res, g);//res+=g;mul(g, g, g);//g<<1k >>= 1;}
}int main() {cin >> k >> m  >> S >> E;memset(g, 0x3f, sizeof g);if (!id.count(S)) id[S] = ++n;if (!id.count(E)) id[E] = ++n;S = id[S], E = id[E];while (m--) {int a, b, c;cin >> c >> a >> b;if (!id.count(a)) id[a] = ++n;if (!id.count(b)) id[b] = ++n;a = id[a], b = id[b];g[a][b] = g[b][a] = min(g[a][b], c);//代表经过一条边}qmi();cout << res[S][E] << endl;return 0;
}

最长路

由于不符合最优子结构无法使用dijkstra,可以用spfa(取反求最短路或改松弛条件为>)

如果全为负可用dijkstra(取反用最短路即可)

相乘的最长路如果符合子结构(例如边权0<w<1),可用dijkstra,否则用spfa

总结:1.spfa 2.拓扑排序(无环可用)上面链接有代码

次短路及k短路

次短路

次短路分为严格次短路非严格次短路,又可分为边可重复次短路边不可重复次短路

  1. 边不可重复的严格最短路(n2logm)

    [P1491 集合位置](P1491 集合位置 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn))

    思路:采用删边法。
    我们先对原先的图跑一边最短路,记录最短路的路径。
    之后每次删去最短路的一条边,重新跑最短路,在这几次跑出来的结果中,取最小值,就是最终答案。

    正确性证明:
    显然,次短路和最短路的路径必然不是相同的。
    因为最短路的路径肯定是整张图中最优的,我们每次去掉最短路上的一条路径,剩下的路径必然不会比最短路的路径更优。
    在删掉一条边后的剩下的路径中取得最短路径,且肯定不会比原先图的最短路径更优,那么这一条路就必然是次短路径。

    较为简单,代码不贴

  2. 边不可重复的非严格最短路(n2logm)

    思路和上面相同,只要求出最短的且满足且不等于最短路就行

  3. 边可重复的严格和非严格最短路((m+n)logm)

    思路:用两个 dist 数组,分别记录最短路和次短路。
    当最短路可以更新时,那么次短路就继承之前的最短路的长度。
    当次短路可以更新,且次短路更新后不会超过最短路长度,那么次短路更新

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    若这道题要求非严格次短路,只需将当次短路可以更新时的后半部分条件稍作修改即可(dist[v][1]<dis_now+w 改成 dist[v][1]<=dis_now+w

    [[USACO06NOV] Roadblocks G]([P2865 USACO06NOV] Roadblocks G - 洛谷 | 计算机科学教育新生态 (luogu.com.cn))

    void dijkstra(){memset(dist,0x3f,sizeof(dist));dist[1][1] = 0;q.push((point){1,0});while (!q.empty()){int tmp = q.top().id;int dis_now = q.top().dis;q.pop();for (int i = head[tmp];i;i = e[i].nxt){int v = e[i].to;int w = e[i].w;if (dist[v][1] > dis_now + w){dist[v][2] = dist[v][1];dist[v][1] = dis_now + w;q.push((point){v,dist[v][1]});q.push((point){v,dist[v][2]});}else if (dist[v][2] > dis_now + w && dist[v][1] < dis_now + w){dist[v][2] = dis_now + w;q.push((point){v,dist[v][2]});}}}}//a*也可做
    
k短路(Astar)(knlogn)

Astar算法可用于求k短路(边可重复),因为该问题搜索空间很大且知道终点,所以适合Astar算法

注意:还可以用于求次短路(严格和不严格)

先反向求一遍所有节点到n的最短路dist[i],以将其作为预估函数
先出队的一定优于后出队的(不再证明),因此n出队k次后即为k短路

acwing 178 第k短路

#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>#define x first
#define y secondusing namespace std;typedef pair<int, int> PII;
typedef pair<int, PII> PIII;
const int N = 1010, M = 200010;int n, m, S, T, K;
int h[N], rh[N], e[M], w[M], ne[M], idx = 1;//建立一个反向头结点方便反向建边
int dist[N], cnt[N];
bool st[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 dijkstra()
{priority_queue<PII,vector<PII>,greater<PII>> heap;heap.push({0,T});//终点memset(dist, 0x3f, sizeof dist);dist[T] = 0;while(heap.size()){auto t = heap.top();heap.pop();int ver = t.y;if(st[ver]) continue;st[ver] = true;for(int i=rh[ver];i;i=ne[i]){int j = e[i];if(dist[j]>dist[ver]+w[i]){dist[j] = dist[ver] + w[i];heap.push({dist[j],j});}}}
}int astar()
{priority_queue<PIII, vector<PIII>, greater<PIII>> heap;// 谁的d[u]+f[u]更小 谁先出队列heap.push({dist[S], {0, S}});while(heap.size()){auto t = heap.top();heap.pop();int ver = t.y.y,distance = t.y.x;cnt[ver]++;//如果终点已经被访问过k次了 则此时的ver就是终点T 返回答案if(cnt[T]==K) return distance;for(int i=h[ver];i!=-1;i=ne[i]){int j = e[i];/* 如果走到一个中间点都cnt[j]>=K,则说明j已经出队k次了,且astar()并没有return distance,说明从j出发找不到第k短路(让终点出队k次),即继续让j入队的话依然无解,那么就没必要让j继续入队了*/if(cnt[j] < K){// 按 真实值+估计值 = d[j]+f[j] = dist[S->t] + w[t->j] + dist[j->T] 堆排// 真实值 dist[S->t] = distance+w[i]heap.push({distance+w[i]+dist[j],{distance+w[i],j}});}}}// 终点没有被访问k次return -1;
}int main()
{cin >> m >> n;for(int i=0;i<n;i++){int a,b,c;cin >> a >> b >> c;add(h,a,b,c);add(rh,b,a,c);}cin >> S >> T >> K;// 起点==终点时 则d[S→S] = 0 这种情况就要舍去 ,总共第K大变为总共第K+1大 if (S == T) K ++ ;// 从各点到终点的最短路距离 作为估计函数f[u]dijkstra();cout << astar();return 0;
}

分层图

问题引入:给你n个点,m条边,每条边都有边权。现在你可以任意选择k条边,使它的边权为0,问从起点到终点的最短路

1.建k层图,类似于种类并查集

2.开dist[n][k],用动态规划来求解

先来看下第一种方法


[P4568 [JLOI2011] 飞行路线]([P4568 JLOI2011] 飞行路线 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn))

1.建图

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图建好后,剩下的就是正常的跑最短路了。但是,有一点需要注意:不见得最优答案会产生在第k层图中。也就是,不见得会跑到第k层图中。

什么时候会出现这种情况呢?当m < k m < km<k的时候。假设m = 1 , k = 10 m = 1, k = 10m=1,k=10,我们只有一条边,也就是说,我们最多建两层图。那么遇到这种情况该怎么办呢?

1.可以在每一层的终点处向下一层的终点处连一条边
2.可以在统计答案的时候在每一层中取最小值

两种方法任取其一

	for (int i = 1; i <= m; i++) {//总共k+1层图int x, y, z;cin >> x >> y >> z;add(x, y, z);add(y, x, z);for (int j = 1; j <= k; j++) {add(x + j * n, y + j * n);add(y + j * n, x + j * n);add(x + (j - 1) * n, y + j * n);add(y + (j - 1) * n, x + j * n);}}dijkstra();int ans = 0x7f7f7f7f;for(int i = 0; i <= k; i ++) ans = min(ans, dis[t + i * n]);//统计每一层的答案,k+1层printf("%d", ans);

最小生成树

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

1.prime(朴素版)
void 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[t] > dist[j]))t = j;if (dist[t] == INF) {cout << "no answer" << endl;return;}res += dist[t];st[t] = true;for (int j = 1; j <= n; j++) dist[j] = min(dist[j], g[t][j]);}cout << res << endl;
}
2.kruskal
int n, m;       // n是点数,m是边数
int p[N];       // 并查集的父节点数组struct Edge     // 存储边
{int a, b, w;bool operator< (const Edge &W)const{return w < W.w;}
}edges[M];int find(int x)     // 并查集核心操作
{if (p[x] != x) p[x] = find(p[x]);return p[x];
}int kruskal()
{sort(edges, edges + m);for (int i = 1; i <= n; i ++ ) p[i] = i;    // 初始化并查集int res = 0, cnt = 0;for (int i = 0; i < m; i ++ ){int a = edges[i].a, b = edges[i].b, w = edges[i].w;a = find(a), b = find(b);if (a != b)     // 如果两个连通块不连通,则将这两个连通块合并{p[a] = b;res += w;cnt ++ ;}}if (cnt < n - 1) return INF;return res;
}
3.次小生成树(倍增)
复杂度

O ( m l o g m ) O(mlogm) O(mlogm)

代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>using namespace std;typedef long long LL;const int N = 100010, M = 300010, INF = 0x3f3f3f3f;int n, m;
struct Edge
{int a, b, w;bool used;bool operator< (const Edge &t) const{return w < t.w;}
}edge[M];
int p[N];
int h[N], e[M], w[M], ne[M], idx;
int depth[N], fa[N][17], d1[N][17], d2[N][17];
int q[N];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)
{if (p[x] != x) p[x] = find(p[x]);return p[x];
}LL kruskal()
{for (int i = 1; i <= n; i ++ ) p[i] = i;sort(edge, edge + m);LL res = 0;for (int i = 0; i < m; i ++ ){int a = find(edge[i].a), b = find(edge[i].b), w = edge[i].w;if (a != b){p[a] = b;res += w;edge[i].used = true;}}return res;
}void build()
{memset(h, -1, sizeof h);for (int i = 0; i < m; i ++ )if (edge[i].used){int a = edge[i].a, b = edge[i].b, w = edge[i].w;add(a, b, w), add(b, a, w);}
}void bfs()
{memset(depth, 0x3f, sizeof depth);depth[0] = 0, depth[1] = 1;q[0] = 1;int hh = 0, tt = 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;fa[j][0] = t;d1[j][0] = w[i], d2[j][0] = -INF;for (int k = 1; k <= 16; k ++ ){int anc = fa[j][k - 1];fa[j][k] = fa[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 w)
{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[fa[a][k]] >= depth[b]){distance[cnt ++ ] = d1[a][k];distance[cnt ++ ] = d2[a][k];a = fa[a][k];}if (a != b){for (int k = 16; k >= 0; k -- )if (fa[a][k] != fa[b][k]){distance[cnt ++ ] = d1[a][k];distance[cnt ++ ] = d2[a][k];distance[cnt ++ ] = d1[b][k];distance[cnt ++ ] = d2[b][k];a = fa[a][k], b = fa[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 (w > dist1) return w - dist1;if (w > dist2) return w - dist2;return INF;
}int main()
{scanf("%d%d", &n, &m);for (int i = 0; i < m; i ++ ){int a, b, c;scanf("%d%d%d", &a, &b, &c);edge[i] = {a, b, c};}LL sum = kruskal();build();bfs();LL res = 1e18;for (int i = 0; i < m; i ++ )if (!edge[i].used){int a = edge[i].a, b = edge[i].b, w = edge[i].w;res = min(res, sum + lca(a, b, w));}printf("%lld\n", res);return 0;
}

最近公共祖先(lca)

1.朴素算法
过程

可以每次找深度比较大的那个点,让它向上跳。显然在树上,这两个点最后一定会相遇,相遇的位置就是想要求的 LCA。 或者先向上调整深度较大的点,令他们深度相同,然后再共同向上跳转,最后也一定会相遇。

性质

朴素算法预处理时需要 dfs 整棵树,时间复杂度为 O ( n ) O(n) O(n),单次查询时间复杂度为 O ( n ) O(n) O(n)外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传。如果树满足随机性质,则时间复杂度与这种随机树的期望高度有关。

2.倍增算法
复杂度

预处理 O ( n l o g n ) O(nlogn) O(nlogn),查询 O ( l o g n ) O(logn) O(logn)

代码
int depth[N], fa[N][16];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);//初始化为INF是为了防止节点重复遍历depth[0] = 0, depth[root] = 1;//depth[0]=0充当哨兵queue<int> q;q.push(root);while (q.size()) {int t = q.front();q.pop();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.push(j);fa[j][0] = t;for (int k = 1; k <= 15; k++)fa[j][k] = fa[fa[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[fa[a][k]] >= depth[b])a = fa[a][k];if (a == b) return a;for (int k = 15; k >= 0; k--)if (fa[a][k] != fa[b][k]) {a = fa[a][k];b = fa[b][k];}return fa[a][0];
}
3.应用

连通性相关


强联通分量
简介

强连通的定义是:有向图 G 强连通是指,G 中任意两个结点连通。

强连通分量(Strongly Connected Components,SCC)的定义是:极大的强连通子图。

这里要介绍的是如何来求强连通分量。

targan算法
时间复杂度

O ( n ) O(n) O(n)

代码
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]) {int y;scc_cnt++;do {y = stk[top--];in_stk[y] = false;id[y] = scc_cnt;siz[scc_cnt]++;} while (y != u);}
}
缩点

我们可以将一张图的每个强连通分量都缩成一个点。

然后这张图会变成一个 DAG,可以进行拓扑排序以及更多其他操作。

对于缩点,缩点后可以构建一张DAG,可以用拓扑排序来得到拓扑序,但这里有一种更简单的方法

根据targan算法的性质,以scc_cnt的降序顺序进行遍历即为拓扑序

set<ll> hask; //哈希函数可以为 a*1000000+b,因为a最大为1e5,这样保证不同的a,b,哈希值不同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 h = a * 1000000ll + b; //a乘与的数根据实际题目范围来定if (a != b && !hask.count(h)) {//如果二者不在同一个强联通分量且边没被加过hask.insert(h);add(fh, a, b);}}for (int i = scc_cnt; i; i--) { //不用再做一遍拓扑排序,按scc_cnt的降序遍历就是拓扑排序//填入操作}
最大半联通子图

[最大半联通子图](AcWing 1175. 最大半连通子图 - AcWing)

问题分析
  • 问题引入:将一个有向图补全为强联通图,至少需要增加几条边?

答案及证明如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

代码
#include<iostream>
#include<set>
#include<cstring>using namespace std;
const int N = 1e5 + 10, M = 2e6 + 10;
using ll = long long;int n, m;
int mod;
int h[N], fh[N], e[M], ne[M], idx = 1;
int dfn[N], low[N], timestamp;
int stk[N], top;
bool in_stk[N];
int scc_cnt, id[N];
int d[N], ans[N];
int siz[N];inline 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]) {int y;scc_cnt++;do {y = stk[top--];id[y] = scc_cnt;in_stk[y] = false;siz[scc_cnt]++;} while (y != u);}
}int main() {cin >> n >> m >> mod;for (int i = 1; i <= m; i++) {int a, b;cin >> a >> b;add(h, a, b);}for (int i = 1; i <= n; i++)if (!dfn[i])tarjan(i);set<ll> hask; //哈希函数可以为 a*1000000+b,因为a最大为1e5,这样保证不同的a,b,哈希值不同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 h = a * 1000000ll + b;if (a != b && !hask.count(h)) {//如果二者不在同一个强联通分量且边没被加过hask.insert(h);add(fh, a, b);}}for (int i = scc_cnt; i; i--) { //不用再做一遍拓扑排序,按scc_cnt的降序遍历就是拓扑排序if (!d[i]) {d[i] = siz[i], ans[i] = 1;//初始化}for (int j = fh[i]; j; j = ne[j]) {int k = e[j];if (d[k] < d[i] + siz[k]) {d[k] = d[i] + siz[k];ans[k] = ans[i];}else if (d[k] == d[i] + siz[k])ans[k] = (ans[k] + ans[i]) % mod;}}int maxn = 0, sum = 0;for(int i=1;i<=scc_cnt;i++)if (d[i] > maxn) {maxn = d[i];sum = ans[i];}else if (d[i] == maxn)sum = (sum + ans[i]) % mod;cout << maxn << endl << sum << endl;return 0;
}
双联通分量
前置概念

桥:对于一个无向图,如果删掉一条边后图中的连通分量数增加了,则称这条边为桥或者割边

割点:对于一个无向图,如果把一个点删除后这个图的极大连通分量数增加了,那么这个点就是这个图的割点(又称割顶)

点联通分量,边联通分量(二者不存在包含关系)

一些概念性质简单介绍:割点 桥 点/边双连通分量(BCC) - tom0727’s blog

边联通分量
解法

targin 算法,与强联通分量相似,当 d f n [ u ] < l o w [ j ] dfn[u]<low[j] dfn[u]<low[j],u,j之间的边就是桥
当dfn[u]==low[u]时,得到新的变联通分量

代码
void tarjan(int u, int from) { //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]) //说明u和j之间的边为桥is_bridge[i] = is_bridge[i ^ 1] = true; //这条边的正边反边都是桥,因为按i^1这样写,所以idx初始要为0}else if (i != (from ^ 1))//如果不走走过的边的反边,也就是重复走low[u] = min(low[u], dfn[j]);}if (dfn[u] == low[u]) {int y;dcc_cnt++;do {y = stk[top--];id[y] = dcc_cnt;} while (y != u);}
}
例题

[acwing \395. 冗余路径](395. 冗余路径 - AcWing题库)

分析:要使任意两点之间都有两条即两条以上不相关的路劲,这显然符合边联通分量的性质,说明可以用tanjan缩点,缩点后的图就是 一颗树,树中的边则是原图中的桥若使这棵树边联通,该树度数为1的节点个数为cnt,答案即为 ( c n t + 1 ) / 2 (cnt+1)/2 (cnt+1)/2

重点

  • 缩点后的图就是一颗树,树中的边则是原图中的桥
  • 若使这棵树边联通,该树度数为1的节点个数为cnt,答案即为 ( c n t + 1 ) / 2 (cnt+1)/2 (cnt+1)/2

结题步骤

  • 缩点
  • 求缩点后度数为一的点的数目

代码

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;const int N = 5010, M = 20010;int n, m;
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_bridge[M];
int d[N];//边联通分量的度数inline void add(int a, int b) {e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}void tarjan(int u, int from) { //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]) //说明u和j之间的边为桥is_bridge[i] = is_bridge[i ^ 1] = true; //这条边的正边反边都是桥,因为按i^1这样写,所以idx初始要为0}else if (i != (from ^ 1))//如果不走走过的边的反边,也就是重复走low[u] = min(low[u], dfn[j]);}if (dfn[u] == low[u]) {int y;dcc_cnt++;do {y = stk[top--];id[y] = dcc_cnt;} while (y != u);}
}int main() {cin >> n >> m;memset(h, -1, sizeof h);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_bridge[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;
}
点联通分量
割点的判断

l o w [ y ] ≤ d f n [ x ] low[y]\leq dfn[x] low[y]dfn[x]时,如下情况时, x x x是割点

  • x x x不是根节点
  • x x x是根节点,但 x x x至少有两个子节点

二分图

匈牙利算法(二分图的最大匹配)
时间复杂度

O ( n m ) O(nm) O(nm)

代码
#include<iostream>
#include<cstring>
using namespace std;const int N = 510, M = 5e4 + 10;
int n, m, t;
int h[N], e[M], ne[M], idx = 1;
int match[N];//代表右边的的点和左边的哪个点匹配,为0则为未匹配
bool st[N];inline void add(int a, int b) {e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}bool find(int x) {for (int i = h[x]; i; i = ne[i]) {int j = e[i];if (!st[j]) {st[j] = true;if (!match[j] || find(match[j])) //如果可以匹配{match[j] = x;return true;}}}return false;
}int main() {cin >> n >> m >> t;while (t--) {int a, b;cin >> a >> b;add(a, b);}int res = 0;for (int i = 1; i <= n; i++)//遍历左边的点,计算最大匹配数{memset(st, false, sizeof st);//清空数组st,st是判断当点i进行匹配时,右边的点是否能匹配if (find(i)) res++;}cout << res << endl;return 0;
}

基础算法

快速排序

#include<iostream>
#include<vector>
using namespace std;int n;
vector<int> nums;void quick_sort(int l, int r) {if (l >= r) return;int i = l - 1, j = r + 1, x = nums[l + r >> 1];while (i < j) {do i++; while (nums[i] < x);do j--; while (nums[j] > x);if (i < j) swap(nums[i], nums[j]);}quick_sort(l, j), quick_sort(j + 1, r);
}int main() {cin >> n;nums.resize(n);for (int i = 0; i < n; i++) cin >> nums[i];quick_sort(0, n - 1);for (auto t : nums) cout << t << " ";return 0;
}

归并排序

#include<iostream>
#include<vector>
using namespace std;int n;
vector<int> nums;
vector<int> p;void merge_sort(int l, int r) {if (l >= r) return;int mid = l + r >> 1, i = l, j = mid + 1;merge_sort(l, mid), merge_sort(mid + 1, r);int k = 0;while (i <= mid && j <= r) {if (nums[i] <= nums[j]) p[k++] = nums[i++];else p[k++] = nums[j++];}while (i <= mid) p[k++] = nums[i++];while (j <= r) p[k++] = nums[j++];for (int i = l, j = 0; i <= r; i++, j++) nums[i] = p[j];
}int main() {cin >> n;nums.resize(n);p.resize(n);for (int i = 0; i < n; i++) cin >> nums[i];merge_sort(0, n - 1);for (auto t : nums) cout << t << " ";return 0;
}

贪心

反悔贪心:【学习笔记】反悔贪心 - Koshkaaa (cnblogs.com)

二分

一种二分模版

int answer=-1;//二分出来的值
while(l<=r){//注意是等于int mid=l+r>>1;if(符合条件) {l=mid+1;answer=max(l,answer);//因为mid为正确值,所以每次取更优的mid}else r=mid-1;
}

优点

1.l和r的取值固定,不需要再变化

2.如果一直二分不到正确的值,不用再判断二分得到的值是否正确,answer事先取一个标志值,如果二分不到正确答案,answer即为标志值不变

取整

1.向下取整:对于正数,c++默认向零取整,即向下取整,或者使用floor()函数
2.向上取整:使用ceil()函数或者,如果对A/B向上取整,可
(A+B-1)/B

3.四舍五入: **round()**函数

卡常

#

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/650970.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LCD液晶显示屏强光老化测试设备太阳光模拟器仪器

1. LCD液晶显示屏老化测试的意义 LCD液晶显示屏老化测试是评估显示屏寿命和性能的重要手段。随着科技的发展&#xff0c;LCD液晶显示屏已经成为我们日常生活中不可或缺的一部分。长期使用后&#xff0c;LCD液晶显示屏可能会出现亮度下降、颜色失真、响应速度变慢等问题。通过进…

DRF 查询(排序、过滤、分页)

查询(排序、过滤、分页) 【0】准备 &#xff08;1&#xff09;Q查询 详细内容可见&#xff1a;Django模型层-CSDN博客Django 的 Q 对象提供了一种在数据库查询中构造复杂查询的方法。当你想在单个查询中组合多个过滤条件&#xff0c;并且这些条件之间不仅仅是简单的 AND 关系…

博睿数据亮相GOPS全球运维大会,Bonree ONE 2024春季正式版发布!

2024年4月25日&#xff0c;博睿数据 Bonree ONE 2024 春季正式版焕新发布。同时&#xff0c;博睿数据AIOps首席专家兼产品总监贺安辉携核心产品新一代一体化智能可观测平台 Bonree ONE 亮相第二十二届 GOPS 全球运维大会深圳站。 Bonree ONE 2024 春季版产品重点升级数据采集、…

Linux实现Nginx的安装与部署并实现html静态资源访问

文章目录 一、安装过程二、常用命令三、查看状态并启动nginx四、放置静态资源五、访问 一、安装过程 1、安装依赖包 yum -y install gcc pcre-devel zlib-devel openssl openssl-devel 2、下载Nginx安装包wget https://nginx.org/download/nginx-1.16.1.tar.gz 3、解压 tar -z…

【学习笔记】Python 使用 matplotlib 画图

文章目录 安装中文显示折线图、点线图柱状图、堆积柱状图坐标轴断点参考资料 本文将介绍如何使用 Python 的 matplotlib 库画图&#xff0c;记录一些常用的画图 demo 代码 安装 # 建议先切换到虚拟环境中 pip install matplotlib中文显示 新版的 matplotlib 已经支持字体回退…

el-date-picker 禁用时分秒选择(包括禁用下拉框展示)

2024.04.26今天我学习了对el-date-picker进行禁用时分秒&#xff0c; 在使用el-date-picker组件的时候&#xff0c;我们有可能遇到需要把时分秒的时间固定&#xff0c;然后并且不能让他修改&#xff1a; 1714120999296 比如右上角的这个时间&#xff0c;我们要给它固定是‘08:…

“AI 程序员入职系列”第二弹:如何利用通义灵码光速改写项目编程语言?

通义灵码入职阿里云云原生团队后&#xff0c;已经展示过 Ta 生成单元测试和自动生成代码的强大实力。今天&#xff0c;阿里云后端工程师云徊将从项目开发的实际需求出发&#xff0c;演示通义灵码在开发工作中可提供的帮助。 通义灵码在 Git 开发项目中起到了哪些作用&#xff…

前端JS必用工具【js-tool-big-box】,防抖和节流的方法调用学习

这一小节&#xff0c;我们针对前端工具包&#xff08;npm&#xff09;js-tool-big-box的使用做一些讲解&#xff0c;主要是防抖和节流方面的。 目录 前言 1 安装和引入 2 防抖的调用学习 3 节流的调用学习 4 使用方法总结 前言 在前端项目中&#xff0c;经常涉及到防抖…

Rancher 应用商店离线环境使用

前言 Rancher (v2.5 ) 应用商店可以方便的安装 Helm3 构建的应用&#xff0c;并且支持私有 helm 应用仓库&#xff0c;方便了内网离线环境下的使用。本文以内网离线环境为前提、以 MySQL 5.7.43 版本为应用举例&#xff0c;从零开始手把手教你如何制作并应用。 1、环境准备 1.…

分布式密钥生成

可验证且无经销商 分布式密钥生成 (DKG) 是一种加密协议&#xff0c;使多方能够协作生成共享密钥&#xff0c;而无需任何一方完全了解密钥。 它通过在多个参与者之间分配信任来增强各种应用程序的安全性&#xff0c;从而降低密钥泄露的风险。 我们引入了一种可验证且无经销商的…

Python实现飞机大战

提供学习或者毕业设计使用&#xff0c;功能基本都有&#xff0c;不能和市场上正式游戏相提比论&#xff0c;请理性对待&#xff01; 本博文将开启免费试读&#xff0c;如有您需要完整源码或者素材材料等&#xff0c;请订阅本专栏或者找博主购买&#xff01;购买后将提供源码文件…

什么是独立服务器?独立服务器最全面介绍

独立服务器是单个客户端具有独占访问权的服务器类型&#xff0c;在服务器开发过程中&#xff0c;以前使用虚拟主机和虚拟独立服务器&#xff08;VPS&#xff09;的公司几乎不可避免地转向独立服务器。 什么是独立服务器&#xff1f; 独立服务器是单个客户端具有独占访问权的服…