算法模版自用(杂)

文章目录

  • 算法
    • 库函数
      • 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.mzph.cn/news/829050.shtml

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

相关文章

内容互动性的提升策略:Kompas.ai的智能工具

在数字营销的新时代&#xff0c;内容的互动性已成为提升用户参与度和品牌忠诚度的关键因素。互动性内容不仅能够吸引用户的注意力&#xff0c;还能够促进用户与品牌的沟通和交流&#xff0c;从而加深用户对品牌的理解和认同。本文将分析互动性内容在提升用户参与度中的作用及其…

基于DEAP数据集的四种机器学习方法的情绪分类

在机器学习领域&#xff0c;KNN&#xff08;K-Nearest Neighbors&#xff09;、SVM&#xff08;Support Vector Machine&#xff09;、决策树&#xff08;Decision Tree&#xff09;和随机森林&#xff08;Random Forest&#xff09;是常见且广泛应用的算法。 介绍 1. KNN&am…

【Java】从0实现一个消息队列中间件

从0实现一个消息队列中间件 什么是消息队列需求分析核心概念核心API交换机类型持久化网络通信网络通信API 消息应答 模块划分项目创建创建核心类创建Exchange创建MSGQueue创建Binding创建Message 数据库设计配置sqlite实现创建表和数据库基本操作 实现DataBaseManager创建DataB…

按现价和不变价计算与公布的统计指标主要有哪些

在经济统计和分析工作中 , 有些指标可以直接用实物量表示 , 如粮食和工业品产量等&#xff1b;而有些指标则是用价值量表示的 , 如全国居民人均可支配收入、社会消费品零售总额、商品房销售额等。在计算价值量指标时&#xff0c;一般均要考虑采用什么价格来计算。统计上常用的价…

设计模式(三):抽象工厂模式

设计模式&#xff08;三&#xff09;&#xff1a;抽象工厂模式 1. 抽象工厂模式的介绍2. 抽象工厂模式的类图3. 抽象工厂模式的实现3.1 创建摩托车的接口3.2 创建摩托车的具体实现3.3 创建汽车的接口3.4 创建汽车的具体产品3.5 创建抽象工厂3.6 创建具体工厂3.7 创建工厂生成器…

苹果一次性开源了8个大模型! 包含模型权重、训练日志和设置,OpenELM全面开源

不以开放性著称的苹果居然同时开源了大模型的权重、训练和评估框架&#xff0c;涵盖训练日志、多个保存点和预训练设置。同时升级计算机视觉工具包 CVNets 为 CoreNet&#xff01;支持 OpenELM&#xff01; ▲图1.由Stable Diffusion3生成。 OpenELM是Apple苹果公司最新推出的…

律师口才训练技巧课程介绍?

律师口才训练技巧课程介绍 一、课程背景与目标 律师口才作为法律职业的核心能力之一&#xff0c;对于律师在**辩论、法律咨询、谈判协商等场合的表现具有至关重要的作用。然而&#xff0c;许多律师在口才方面存在不足&#xff0c;难以充分发挥自己的专业能力。因此&#xff0c;…

底层逻辑(1) 是非对错

底层逻辑(1) 是非对错 关于本书 这本书的副标题叫做&#xff1a;看清这个世界的底牌。让我想起电影《教父》中的一句名言&#xff1a;花半秒钟就看透事物本质的人&#xff0c;和花一辈子都看不清事物本质的人&#xff0c;注定是截然不同的命运。 如果你看过梅多丝的《系统之美…

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

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

WildCard开通GitHub Copilot

更多AI内容请关注我的专栏&#xff1a;《体验AI》 期待您的点赞&#x1f44d;收藏⭐评论✍ WildCard开通GitHub Copilot GitHub Copilot 简介主要功能工作原理 开通过程1、注册Github账号2、准备一张信用卡或虚拟卡3、进入github copilot页4、选择试用5、选择支付方式6、填写卡…

为什么单片机控制电机需要加电机驱动

通常很多地方只是单纯的单片机MCU没有对电机的驱动能力&#xff0c;或者是介绍关于电机驱动的作用&#xff0c;如&#xff1a; 提高电机的效率和精度。驱动器采用先进的电子技术和控制算法&#xff0c;能够精准控制电机的参数和运行状态&#xff0c;提高了电机的效率和精度。拓…

【Hello算法】 > 第 3 关 >栈与队列

数据结构 之 数组与链表 1 栈 / 栈的常见操作、实现、应用2 队列 /队列的常见操作、实现、应用3 双向队列4 Tips ———————————————————————————————————————————————————————————- ————————————————…

Hybrid Homomorphic Encryption:SE + HE

参考文献&#xff1a; [NLV11] Naehrig M, Lauter K, Vaikuntanathan V. Can homomorphic encryption be practical?[C]//Proceedings of the 3rd ACM workshop on Cloud computing security workshop. 2011: 113-124.[MJS16] Maux P, Journault A, Standaert F X, et al. To…

STM32应用开发教程进阶--UART串口重定向(printf)

实现目标 1、掌握STM32 HAL库的串口重定向 2、具体目标&#xff1a;1、实现printf “打印”各种常用的类型的数据变量 一、串口“打印” UART串口通信协议是我们常用的通信协议&#xff08;UART、I2C、SPI等&#xff09;之一&#xff0c;全称叫做通用异步收发传输器&#xf…

Druid高性能数据库连接池?SpringBoot整合MyBatis整合SpringMVC整合Druid

文章目录 Druid高性能数据库连接池&#xff1f;SpringBoot整合MyBatis整合SpringMVC整合Druid异常记录spring-boot-starter-parent作用Druid介绍什么是数据库连接池&#xff1f;为什么选择Druid数据库连接池整合SpringBoot,MyBatis,SpringMVC,Druid到Maven项目的真个流程pom文件…

OSPF域间路由防环原则

1.域间路由防环原则 ①原则一 1&#xff09;为了避免区域间的环路&#xff0c;OSPF规定不同区域间的路由交互只能通过ABR实现。 2&#xff09;ABR是连接到骨干区域的&#xff0c;所以在区域设计上规定&#xff0c;所有非骨干区域都要连接到骨干区域。区 域间的通讯需要通…

C语言进阶:进阶指针(下)

一、 函数指针数组 我们都知道 数组是一个存放相同类型数据的存储空间 那我们已经学习了指针数组 那么函数有没有对应的指针数组呢&#xff1f; 如果有那应该怎么定义呢&#xff1f; 1. 函数指针数组的定义 我们说 函数指针数组的定义 应该遵循以下格式 int (*p[10])(); 首…

SpringBoot Aop使用篇

Getting Started SpringBoot AOP的实践 AOP相关的概念&#xff1a; Aspect&#xff08;切面&#xff09;&#xff1a; Aspect 声明类似于 Java 中的类声明&#xff0c;在 Aspect 中会包含着一些 Pointcut 以及相应的 Advice。就是抽离出来的逻辑类&#xff0c;比如日志、权限…

C++及QT的线程学习

目录 一. 线程学习 二. 学习线程当中&#xff0c;得到的未知。 1. 了解以下MainWindow和main的关系 2. []()匿名函数 有函数体&#xff0c;没有函数名. 3. join和detach都是用来管理线程的生命周期的&#xff0c;它们的区别在于线程结束和资源的回收。 4. operator()() 仿…

4G组网三相四线预付费电表-远程集中抄表

安科瑞薛瑶瑶18701709087/17343930412 DTSY1352 三相预付费电能表分别用于计量额定频率50Hz 的单、三相交流有功电能&#xff0c;具有预付费控制、负载控制、时间控制及 RS485 通信等功能&#xff0c;性能指标符合 GB/T17215.321-2008 标准。是改革传统用电体制&#xff0c…