文章目录
- 8.数据结构
-
- [8.1 队列](#8.1 队列)
-
- [8.1.1 单调队列](#8.1.1 单调队列)
- [8.2 栈](#8.2 栈)
-
- [8.2.1 单调栈](#8.2.1 单调栈)
- [8.3 链表](#8.3 链表)
- [8.4 树](#8.4 树)
-
- [8.4.1 二叉树](#8.4.1 二叉树)
- [8.4.2 二叉堆](#8.4.2 二叉堆)
- [8.4.3 并查集](#8.4.3 并查集)
- [8.4.4 树状数组](#8.4.4 树状数组)
- [9.4.5 字典树](#9.4.5 字典树)
- [8.5 图](#8.5 图)
- [8.6 分块](#8.6 分块)
- 9.图论
-
- [9.1 最小生成树](#9.1 最小生成树)
-
- [9.1.1 Prim算法](#9.1.1 Prim算法)
- [9.1.2 Kruskal算法](#9.1.2 Kruskal算法)
- [9.2 拓扑排序](#9.2 拓扑排序)
- [9.3 最短路](#9.3 最短路)
-
- [9.3.1 Dijkstra算法](#9.3.1 Dijkstra算法)
- [9.3.2 SPFA算法](#9.3.2 SPFA算法)
- [10.3.3 Floyd算法](#10.3.3 Floyd算法)
- [9.4 负环](#9.4 负环)
- [9.5 二分图](#9.5 二分图)
- [9.6 欧拉路径](#9.6 欧拉路径)
8.数据结构
8.1 队列
8.1.1 单调队列
有一个长为 n 的序列,以及一个大小为 k 的窗口。现在这个从左边开始向右滑动,每次滑动一个单位,求出每次滑动后窗口中的最大值和最小值。
单调队列与普通队列不一样的地方就在于单调队列既可以从队首出队,也可以从队尾出队。用deque。
cpp
#include<bits/stdc++.h>
using namespace std;
struct Node
{
int v,pos;
}node[1000050];
deque<Node>q;
int n,k;
int main()
{
cin>>n>>k;
for(int i=1;i<=n;i++)
{
scanf("%d",&node[i].v);
node[i].pos=i;
}
for(int i=1;i<=n;i++)
{
while(q.size()&&node[i].v<q.back().v)
q.pop_back();//从后端弹出不优的结点(不优:下标小,数值还大)
q.push_back(node[i]);
while(q.size()&&q.front().pos<i-k+1)
q.pop_front();//从前端弹出窗口范围外的节点
if(i>=k) printf("%d ",q.front().v);
}
cout<<endl;
q.clear();
for(int i=1;i<=n;i++)
{
while(q.size()&&node[i].v>q.back().v)
q.pop_back();
q.push_back(node[i]);
while(q.size()&&q.front().pos<i-k+1)
q.pop_front();
if(i>=k) printf("%d ",q.front().v);
}
return 0;
}
滑动窗口最大值
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字,滑动窗口每次只向右移动一位。返回滑动窗口中的最大值数组。
单调递减双端队列。队列存
{值, 下标},新元素入队前从队尾弹出所有比它小的元素(它们永远不可能成为最大值);队首滑出窗口时弹出。队首始终是当前窗口最大值。时间 O(n)。
cpp
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
struct Node { int v, pos; };
deque<Node> q;
vector<int> ans;
for (int i = 0; i < nums.size(); i++) {
// 维护单调递减(新元素大,队尾就永无出头之日)
while (!q.empty() && q.back().v < nums[i]) q.pop_back();
q.push_back({nums[i], i});
// 弹出窗口滑出的元素
if (q.front().pos <= i - k) q.pop_front();
if (i >= k - 1) ans.push_back(q.front().v);
}
return ans;
}
};
8.2 栈
8.2.1 单调栈
快速获得每个数之后一个比它大(小)的元素,时间复杂度为O ( n )
cpp
#include<bits/stdc++.h>
using namespace std;
const int N=3000050;
int ans[N],a[N],st[N];
//a:数据数组
//st:模拟栈(储存的是数值的对应下标)(单调递减栈)
//ans:储存每个下标对应的答案
int n,k=0;
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
while(a[i]>a[st[k]]&&k)
ans[st[k--]]=i;//取走栈内比当前元素大的元素,并且更新这些被取走元素的答案
st[++k]=i;//将当前元素放入栈内
}
for(int i=1;i<=n;i++)
printf("%d ",ans[i]) ;
return 0;
}
有效的括号
给定一个只包括 '('、')'、'{'、'}'、'['、']' 的字符串 s,判断字符串是否有效。有效字符串需满足:左括号必须用相同类型的右括号闭合;左括号必须以正确的顺序闭合;每个右括号都有一个对应的相同类型的左括号。
栈匹配。左括号入栈;右括号检查栈顶是否为对应左括号,不匹配或栈空则返回 false,匹配则弹出。遍历结束栈空说明全部闭合。时间 O(n),空间 O(n)。
cpp
class Solution {
public:
bool isValid(string s) {
stack<char> st;
for (char c : s) {
// 左括号入栈
if (c == '(' || c == '[' || c == '{') {
st.push(c);
}
// 右括号:检查栈顶是否匹配
else {
if (st.empty()) return false; // 没有对应的左括号
char top = st.top();
if ((c == ')' && top != '(') ||
(c == ']' && top != '[') ||
(c == '}' && top != '{')) {
return false;
}
st.pop();
}
}
return st.empty(); // 栈空说明所有左括号都闭合了
}
};
最小栈
设计一个支持 push、pop、top 操作,并能在常数时间 内检索到最小元素的栈。实现 MinStack 类:push(val) 将元素压入栈;pop() 删除栈顶元素;top() 获取栈顶元素;getMin() 检索栈中的最小元素。
双栈(数据栈 + 最小值栈)。
minSt栈顶始终是当前栈内最小值,push时压入min(value, minSt.top()),pop时两栈同步弹出;minSt初始压入INT_MAX作哨兵避免判空。getMin直接返回minSt.top(),所有操作 O(1)。
cpp
class MinStack {
private:
stack<int> st; // 数据栈
stack<int> minSt; // 最小值栈
public:
MinStack() {
minSt.push(INT_MAX); // 哨兵,避免 push 时判空
}
void push(int value) {
st.push(value);
minSt.push(min(value, minSt.top()));
}
void pop() {
st.pop();
minSt.pop();
}
int top() {
return st.top();
}
int getMin() {
return minSt.top();
}
};
/**
* Your MinStack object will be instantiated and called as such:
* MinStack* obj = new MinStack();
* obj->push(value);
* obj->pop();
* int param_3 = obj->top();
* int param_4 = obj->getMin();
*/
字符串解码
给定一个经过编码的字符串,返回它解码后的字符串。编码规则为 k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。可以认为输入字符串总是有效的,且原字符串中不含有额外的空格。
双栈(数字栈 + 字符串栈)。遇到数字累积
num;遇到[把num和当前字符串分别压栈,然后清空;遇到]弹出次数和之前的字符串,把当前字符串重复times次拼到之前字符串后面;普通字符追加到当前字符串。时间 O(n + 输出长度),空间 O(n)。
cpp
class Solution {
public:
string decodeString(string s) {
stack<int> numStk;
stack<string> strStk;
string cur = "";
int num = 0;
for (char c : s) {
if (isdigit(c)) {
num = num * 10 + (c - '0');
} else if (c == '[') {
numStk.push(num);
strStk.push(cur);
num = 0;
cur = "";
} else if (c == ']') {
int times = numStk.top(); numStk.pop();
string prev = strStk.top(); strStk.pop();
string tmp = "";
for (int i = 0; i < times; i++) tmp += cur;
cur = prev + tmp;
} else {
cur += c;
}
}
return cur;
}
};
- 每日温度
给定一个整数数组 temperatures,表示每天的温度,返回一个数组 answer,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。
单调递减栈(存下标)。遍历温度,当前温度大于栈顶温度时,栈顶下标的答案就是
i - idx,弹出;否则入栈。栈内温度单调递减,每个下标最多入栈出栈一次。时间 O(n),空间 O(n)。
cpp
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
int n = temperatures.size();
vector<int> ans(n, 0); // 初始化,默认 0
stack<int> st; // 只存索引
for (int i = 0; i < n; i++) {
while (!st.empty() && temperatures[i] > temperatures[st.top()]) {
int idx = st.top();
st.pop();
ans[idx] = i - idx;
}
st.push(i);
}
return ans;
}
};
柱状图中最大的矩形
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1。求在该柱状图中,能够勾勒出来的矩形的最大面积。
单调递增栈 + 首尾哨兵。首尾各加高度 0 的哨兵,保证栈不空且所有柱子最终被弹出计算。遍历高度,当前高度小于栈顶高度时弹出栈顶,以弹出柱子的高度为矩形高,宽度为
i - 新栈顶 - 1,更新最大面积。时间 O(n),空间 O(n)。
cpp
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
heights.insert(heights.begin(), 0); // 首部哨兵
heights.push_back(0); // 尾部哨兵
stack<int>st;
int ans=0;
for(int i=0;i<heights.size();i++)
{
while(!st.empty()&&heights[i]<heights[st.top()])
{
int h=heights[st.top()];
st.pop();
int w=i-st.top()-1;
ans=max(ans,h*w);
}
st.push(i);
}
return ans;
}
};
8.3 链表
特殊约瑟夫问题(链表):
编号为1...N的N个小朋友玩游戏,他们按编号顺时针围成一圈,从第一个人开始按逆时针次序报数,报到第M个人出列;然后再从下个人开始按顺时针次序报数,报到第K个人出列;再从下一个人开始按逆时针次序报数,报到第M个人出列;再从下个人开始按顺时针次序报数,报到第K个人出列......以此类推不断循环,直至最后一人出列。请编写程序按顺序输出出列人的编号。
cpp
#include<bits/stdc++.h>
using namespace std;
typedef struct tagNode
{
int data;
struct tagNode *pre,*next;
}Node,*LinkList;
void InitList(LinkList *L)
{
*L=(LinkList)malloc(sizeof(Node));
//获取Node的字段长度,然后强转为LinkList类型。L变量就代表地址长
度和Node一样所占内存空间同样大小的LinkList
(*L)->next=(*L)->pre=*L;
//初始化:两个指针都指向自身
}
int main()
{
int n,m,k,i;
cin>>n>>m>>k;
LinkList L;
InitList(&L);
struct tagNode *p,*q,*g;
p=(LinkList)malloc(sizeof(Node));
g=q=p;
p->data=1;
p->next=p;
for(i=2;i<=n;i++)
{
q = (q->next=(LinkList)malloc(sizeof(Node)));
q->pre=g;
q->data=i;
q->next=p;
g=g->next;
}
p->pre=g;
q=p;
for(int cnt=1;cnt<=n;cnt++)
{
if(cnt&1)
{
if (m == 1)
{
printf("%d", q->data);
q->next->pre = q->pre;
q->pre->next = q->next;
q = q->pre;
}
else
{
for (i=2;i<m;i++)
q=q->pre;
printf("%d ",q->pre->data);
q->pre=q->pre->pre;
q->pre->next=q;
q = q->pre;
}
}
else
{
if (k == 1)
{
printf("%d", q->data);
q->next->pre=q->pre;
q->pre->next=q->next;
q = q->next;
}
else
{
for (i=2;i<k; i++)
q=q->next;
printf("%d ", q->next->data);
q->next=q->next->next;
q->next->pre=q;
q=q->next;
}
}
}
return 0;
}
相交链表
给你两个单链表的头节点 headA 和 headB,请你找出并返回两个单链表相交的起始节点 。如果两个链表不存在相交节点,返回 null。
双指针消除长度差。指针 A 遍历完 A 后转到 B 头,指针 B 遍历完 B 后转到 A 头;两指针走的总步数相同,最终会在相交节点相遇。若无交点,则同时走到
nullptr。时间 O(m+n),空间 O(1)。
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode *A = headA, *B = headB;
while (A != B) {
A = A != nullptr ? A->next : headB;
B = B != nullptr ? B->next : headA;
}
return A;
}
};
反转链表
给你单链表的头节点 head,请你反转链表,并返回反转后的链表。
迭代三指针。用
prev、curr、next,每次把curr->next指向prev,然后三个指针整体后移。最终prev就是新头。时间 O(n),空间 O(1)。
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* pre=nullptr;
ListNode* cur=head;
while(cur){
ListNode* next=cur->next;
cur->next=pre;
pre=cur;
cur=next;
}
return pre;
}
};
回文链表
给你一个单链表的头节点 head,请你判断该链表是否为回文链表 。如果是,返回 true;否则,返回 false。
快慢指针找中点 + 反转后半 + 比较。快慢指针找到中点后,反转后半部分,再与前半部分逐一比较;比较完可再反转回来恢复原链表(可选)。时间 O(n),空间 O(1)。
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
bool isPalindrome(ListNode* head) {
if (!head || !head->next) return true;
// 1. 快慢指针找中点
ListNode* slow = head;
ListNode* fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
// 2. 反转后半部分
ListNode* secondHalf = reverseList(slow);
// 3. 比较前半部分和后半部分
ListNode* firstHalf = head;
while (secondHalf) {
if (firstHalf->val != secondHalf->val) {
return false;
}
firstHalf = firstHalf->next;
secondHalf = secondHalf->next;
}
return true;
}
private:
ListNode* reverseList(ListNode* head) {
ListNode* prev = nullptr;
ListNode* curr = head;
while (curr) {
ListNode* next = curr->next;
curr->next = prev;
prev = curr;
curr = next;
}
return prev;
}
};
环形链表
给你一个链表的头节点 head,判断链表中是否有环 。如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。
快慢指针判环。慢指针走一步,快指针走两步;若相遇则有环,若快指针走到
nullptr则无环。时间 O(n),空间 O(1)。
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
bool hasCycle(ListNode *head) {
if(head==nullptr||head->next==nullptr){
return false;
}
ListNode* slow=head;
ListNode* fast=head->next;
while(slow!=fast){
if(fast==nullptr||fast->next==nullptr)
return false;
slow=slow->next;
fast=fast->next->next;
}
return true;
}
};
环形链表 II
给定一个链表的头节点 head,返回链表开始入环的第一个节点 。如果链表无环,则返回 null。
快慢指针相遇 + 双指针找入口。快慢指针相遇后,一个指针从
head出发,另一个从相遇点出发,同速前进,再次相遇即为环入口。时间 O(n),空间 O(1)。
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode *slow = head, *fast = head;
while (fast != nullptr) {
slow = slow->next;
if (fast->next == nullptr) {
return nullptr;
}
fast = fast->next->next;
if (fast == slow) {
ListNode *ptr = head;
while (ptr != slow) {
ptr = ptr->next;
slow = slow->next;
}
return ptr;
}
}
return nullptr;
}
};
合并两个有序链表
将两个升序 链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
哑节点 + 双指针。用
dummy简化头节点处理,比较两链表当前节点,较小者接到结果尾部;某条链表走完后,把另一条剩余部分直接接上。时间 O(m+n),空间 O(1)。
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
ListNode* dum = new ListNode(0);
ListNode* cur = dum;
while (list1 != nullptr && list2 != nullptr) {
if (list1->val < list2->val) {
cur->next = list1;
list1 = list1->next;
}
else {
cur->next = list2;
list2 = list2->next;
}
cur = cur->next;
}
cur->next = list1 != nullptr ? list1 : list2;
return dum->next;
}
};
两数相加
给你两个非空 的链表,表示两个非负的整数。它们每位数字都是按照逆序的方式存储的,并且每个节点只能存储一位数字。请你将两个数相加,并以相同形式返回一个表示和的链表。
模拟竖式加法。用
carry记录进位,循环条件为l1 || l2 || carry,每次求和取模得当前位、除以 10 得进位;用哑节点简化链表构建。时间 O(max(m,n)),空间 O(max(m,n))。
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode* dummy = new ListNode(0);
ListNode* cur = dummy;
int carry = 0;
while (l1 || l2 || carry) {
int sum = carry;
if (l1) {
sum += l1->val;
l1 = l1->next;
}
if (l2) {
sum += l2->val;
l2 = l2->next;
}
carry = sum / 10;
cur->next = new ListNode(sum % 10);
cur = cur->next;
}
return dummy->next;
}
};
删除链表的倒数第 N 个结点
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
哑节点 + 快慢指针。
fast先走n+1步,然后fast和slow同步前进;fast到nullptr时,slow正好在待删节点的前一个,执行slow->next = slow->next->next。时间 O(n),空间 O(1)。
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(0, head);
ListNode* fast = dummy;
ListNode* slow = dummy;
// fast 先走 n+1 步,让 slow 和 fast 之间间隔 n 个节点
for (int i = 0; i <= n; i++) {
fast = fast->next;
}
// 然后一起走,fast 到末尾时,slow 就在倒数第 n 个的前一个
while (fast) {
fast = fast->next;
slow = slow->next;
}
// 删除
slow->next = slow->next->next;
return dummy->next;
}
};
两两交换链表中的节点
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即只能进行节点交换)。
哑节点 + 迭代交换。用
pre指向每对节点的前驱,first、second为待交换的两个节点;交换三步:pre->next = second、first->next = second->next、second->next = first,然后pre = first进入下一对。时间 O(n),空间 O(1)。
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
ListNode* dummy = new ListNode(0, head);
ListNode* pre = dummy;
while (pre->next && pre->next->next) {
ListNode* first = pre->next; // 第一个节点
ListNode* second = pre->next->next; // 第二个节点
// 开始交换
pre->next = second; // 前驱指向第二个(新的头)
first->next = second->next; // 第一个指向第三个
second->next = first; // 第二个指向第一个
// 前驱前进两个位置
pre = first;
}
return dummy->next;
}
};
K 个一组翻转链表
给你链表的头节点 head,每 k 个节点一组进行翻转,请你返回修改后的链表。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
分组 + 头插法翻转。先检查剩余节点是否够
k个,不够则直接返回;用pre作为每组的前驱,tail为第k个节点,nxt = tail->next为下一组开头;用头插法把pre->next到tail之间的节点逐个插到pre后面,完成后pre = cur(原组头,翻转后变组尾)继续下一组。时间 O(n),空间 O(1)。
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* reverseKGroup(ListNode* head, int k) {
ListNode* dummy = new ListNode(0, head);
ListNode* pre = dummy; // 每次要翻转的 k 个节点的前一个
while (true) {
// 检查是否够 k 个
ListNode* tail = pre;
for (int i = 0; i < k; i++) {
tail = tail->next;
if (!tail) return dummy->next;
}
// 头插法翻转 k 个节点
ListNode* cur = pre->next; // 当前段的头(翻转后变尾)
ListNode* nxt = tail->next; // 下一段的头
// 把 cur 后面的节点一个个插到 pre 后面
while (cur->next != nxt) {
ListNode* temp = cur->next;
cur->next = temp->next;
temp->next = pre->next;
pre->next = temp;
}
pre = cur; // pre 跳到尾部,准备下一段
}
}
};
随机链表的复制
给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random,该指针可以指向链表中的任何节点或空节点。构造这个链表的深拷贝。
哈希表映射。第一遍遍历原链表,为每个节点创建新节点,建立
原节点 -> 新节点的映射;第二遍遍历,根据映射设置新节点的next和random。时间 O(n),空间 O(n)。进阶 O(1) 空间:把新节点插在原节点后面,再拆分。
cpp
/*
// Definition for a Node.
class Node {
public:
int val;
Node* next;
Node* random;
Node(int _val) {
val = _val;
next = NULL;
random = NULL;
}
};
*/
class Solution {
public:
Node* copyRandomList(Node* head) {
if (!head) return nullptr;
unordered_map<Node*, Node*> mp;
// 第一遍:创建所有新节点,建立映射
Node* cur = head;
while (cur) {
mp[cur] = new Node(cur->val);
cur = cur->next;
}
// 第二遍:连接 next 和 random
cur = head;
while (cur) {
mp[cur]->next = mp[cur->next]; // 新节点的 next
mp[cur]->random = mp[cur->random]; // 新节点的 random
cur = cur->next;
}
return mp[head];
}
};
合并 K 个升序链表
给你一个链表数组,每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中,返回合并后的链表。
优先队列(小根堆)。把所有链表头节点入堆,每次弹出最小节点接到结果尾部,若该节点有
next则入堆。时间 O(N log k)(N 为总节点数,k 为链表数),空间 O(k)。
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
struct comp {
bool operator()(ListNode* a, ListNode* b) {
return a->val > b->val;
}
};
priority_queue<ListNode*, vector<ListNode*>, comp> q;
ListNode* mergeKLists(vector<ListNode*>& lists) {
for (auto node: lists) {
if (node) q.push(node);
}
ListNode* head = new ListNode();
ListNode* tail = head;
while (!q.empty()) {
ListNode* node = q.top();
q.pop();
tail->next = node;
tail = tail->next;
if (node->next) q.push(node->next);
}
return head->next;
}
};
8.4 树
8.4.1 二叉树
二叉树叶结点值和最大层
已知一棵非空二叉树结点的数据域为不等于0的整数,请编写程序找出该二叉树中叶结点数据值之和最大的层。
cpp
#include<bits/stdc++.h>
using namespace std;
int sum[101],total,depth=1;
typedef struct Binary_tree_node
{
int data;
struct Binary_tree_node *left_leave,*right_leave;
}Binary_tree_node,*Link;
Link create()
{
int num;
Link node;
cin>>num;
if(!num)
node=NULL;
else
{
node=(Link)malloc(sizeof(Binary_tree_node));
node->data=num;
node->left_leave=create();
node->right_leave=create();
}
return node;
}
void fuc(Link BT)
{
if(BT)
{
if(BT->left_leave==NULL&&BT->right_leave==NULL)
sum[depth]+=BT->data;
fuc(BT->left_leave);
fuc(BT->right_leave);
if(BT->left_leave!=NULL&&BT->right_leave!=NULL)
depth++;
}
return;
}
int cmp(int *arr,int depth)
{
int i,temp=arr[0],index=0;
for(i=1;i<depth;i++)
{
if(arr[i]>=temp)
{
temp=arr[i];
index=i;
}
}
return index;
}
int main()
{
Link tree=NULL;
tree=create();
fuc(tree);
int ans=cmp(sum,depth);
cout<<ans;
return 0;
}
前序和中序构造二叉树
本题目要求用先序序列和中序序列构造一棵二叉树(树中结点个数不超过10个),并输出其后序序列。
cpp
#include<bits/stdc++.h>
using namespace std;
int n;
int front[11],mid[11];
void fun(int ll,int lr,int rl,int rr)
{
if(ll>lr)
return;
int pos;
int t=front[ll];
for(int i=rl;i<=rr;i++)
{
if(mid[i]==t)
{
pos=i;
break;
}
}
fun(ll+1,ll+pos-rl,rl,pos-1);
fun(ll+pos-rl+1,lr,pos+1,rr);
cout<<t<<' ';
}
int main()
{
cin>>n;
for(int i=0;i<n;i++)
cin>>front[i];
for(int i=0;i<n;i++)
cin>>mid[i];
fun(0,n-1,0,n-1);
return 0;
}
给前序和中序,层序输出
cpp
#include<bits/stdc++.h>
using namespace std;
int pre[35],in[35],ltree[35],rtree[35];
int n;
queue<int>q;
int build(int l1,int r1,int l2,int r2)
{
if(l1>r1) return 0;
int root=pre[l2];
int pos=l1;
while(in[pos]!=root) pos++;
int len=pos-l1;
ltree[root]=build(l1,pos-1,l2+1,l2+len);
rtree[root]=build(pos+1,r1,l2+len+1,r2);
return root;
}
void level(int n)
{
int root=pre[n];
cout<<root;
if(ltree[root]) q.push(ltree[root]);
if(rtree[root]) q.push(rtree[root]);
while(q.size())
{
int now=q.front();
q.pop();
cout<<' '<<now;
if(ltree[now]) q.push(ltree[now]);
if(rtree[now]) q.push(rtree[now]);
}
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
scanf("%d",&in[i]);
for(int i=1;i<=n;i++)
scanf("%d",&pre[i]);
build(1,n,1,n);
level(1);
return 0;
}
后序和中序构造二叉树
本题目要求用后序序列和中序序列构造一棵二叉树(树中结点个数不超过10个),并输出其先序序列。
cpp
#include<bits/stdc++.h>
using namespace std;
int n;
int mid[11],back[11];
void fun(int ll,int lr,int rl,int rr)
{
if(ll>lr)
return;
cout<<back[lr]<<' ';
int pos;
int t=back[lr];
for(int i=rr;i>=rl;i--)
{
if(mid[i]==t)
{
pos=i;
break;
}
}
fun(ll,pos+ll-rl-1,rl,pos-1);
fun(pos+ll-rl,lr-1,pos+1,rr);
}
int main()
{
cin>>n;
for(int i=0;i<n;i++)
cin>>back[i];
for(int i=0;i<n;i++)
cin>>mid[i];
fun(0,n-1,0,n-1);
return 0;
}
给中序和后序,层序输出
cpp
#include<bits/stdc++.h>
using namespace std;
int in[35],post[35],ltree[35],rtree[35];
int n;
queue<int>q;
int build(int l1,int r1,int l2,int r2)
{
//第1,2,3,4个参数分别为所要建的树的中序左端点,中序右端点,后序左端点,后序右端点
if(l1>r1) return 0;
int root=post[r2];
int pos=l1;
while(in[pos]!=root) pos++;//在中序中找到(相对的)根结点的位置,在该位置左边的为左子树序列,右边的为右子树序列
int len=pos-l1;
ltree[root]=build(l1,pos-1,l2,l2+len-1);//建左子树
rtree[root]=build(pos+1,r1,l2+len,r2-1);//建右子树
return root;//该点是上一层的左孩子or右孩子,返回到上层
}
void level(int n)
{
int root=post[n];//后序的最后一个为总的根结点
cout<<root;
if(ltree[root]) q.push(ltree[root]);
if(rtree[root]) q.push(rtree[root]);
while(q.size())
{
int now=q.front();
q.pop();
cout<<' '<<now;
if(ltree[now]) q.push(ltree[now]);
if(rtree[now]) q.push(rtree[now]);
}
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
scanf("%d",&post[i]);//输入后序
for(int i=1;i<=n;i++)
scanf("%d",&in[i]);//输入中序
build(1,n,1,n);//建树
level(n);//层序输出
return 0;
}
二叉树的中序遍历
给定一个二叉树的根节点 root,返回它的中序遍历结果(左 → 根 → 右)。
递归。按"左 → 根 → 右"顺序递归遍历,把节点值加入结果数组。时间 O(n),空间 O(h)(递归栈,h 为树高)。也可用栈迭代模拟。
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
private:
void inorder(TreeNode* node, vector<int>& res) {
if (!node) return;
inorder(node->left, res); // 左
res.push_back(node->val); // 根
inorder(node->right, res); // 右
}
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> ans;
inorder(root,ans);
return ans;
}
};
二叉树的最大深度
给定一个二叉树 root,返回其最大深度。最大深度是指从根节点到最远叶子节点的最长路径上的节点数。
递归。空节点返回 0,否则返回
max(左子树深度, 右子树深度) + 1。时间 O(n),空间 O(h)。
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int maxDepth(TreeNode* root) {
if (root == nullptr) {
return 0;
}
int left_depth = maxDepth(root->left);
int right_depth = maxDepth(root->right);
return max(left_depth, right_depth) + 1;
}
};
翻转二叉树
给你一棵二叉树的根节点 root,翻转这棵二叉树(左右子树交换),并返回其根节点。
递归交换。先递归翻转左右子树,再交换当前节点的左右指针。时间 O(n),空间 O(h)。也可用队列做层序翻转。
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
if(root == nullptr) return nullptr;
invertTree(root -> left);
invertTree(root -> right);
TreeNode* temp = root ->left;
root -> left = root ->right;
root ->right = temp;
return root;
}
};
对称二叉树
给你一个二叉树的根节点 root,检查它是否轴对称。
双指针递归比较。写一个辅助函数
isMirror(left, right),判断两棵树是否镜像:两节点都空返回 true,一个空返回 false,值相等且isMirror(left->left, right->right)和isMirror(left->right, right->left)都成立。时间 O(n),空间 O(h)。
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
bool isSymmetric(TreeNode* root) {
if (root == nullptr) return true;
return isMirror(root->left, root->right);
}
bool isMirror(TreeNode* left, TreeNode* right) {
if (left == nullptr && right == nullptr) return true;
if (left == nullptr || right == nullptr) return false;
return (left->val == right->val) &&
isMirror(left->left, right->right) &&
isMirror(left->right, right->left);
}
};
二叉树的直径
给你一棵二叉树的根节点,返回该树的直径 。二叉树的直径是指树中任意两个节点之间最长路径的长度,这条路径可能经过也可能不经过根节点。直径的长度以路径上的边数表示。
后序 DFS + 全局最大值。递归返回以当前节点为端点向下的最长链(边数),同时用
left + right更新全局最大直径(经过当前节点的路径)。空节点返回 -1,使叶子节点返回 0。时间 O(n),空间 O(h)。
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int ans = 0;
int dfs(TreeNode* node) {
if (!node) return -1; // 空子树不贡献边
int left = dfs(node->left) + 1;
int right = dfs(node->right) + 1;
// 经过 node 的路径长度 = left + right
ans = max(ans, left + right);
// 返回以 node 为端点向下的最长链
return max(left, right);
}
int diameterOfBinaryTree(TreeNode* root) {
dfs(root);
return ans;
}
};
二叉树的层序遍历
给你二叉树的根节点 root,返回其节点值的层序遍历(即逐层地,从左到右访问所有节点)。
BFS + 队列。队列存节点,每轮先记录当前层节点数
levelSize,循环levelSize次弹出节点、收集值、把左右孩子入队,得到当前层结果。时间 O(n),空间 O(w)(w 为最大层宽)。
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> res;
if (!root) return res; // 空树直接返回
queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
int levelSize = q.size(); // 当前层的节点数
vector<int> level; // 存放当前层的值
for (int i = 0; i < levelSize; i++) {
TreeNode* node = q.front();
q.pop();
level.push_back(node->val);
if (node->left) q.push(node->left);
if (node->right) q.push(node->right);
}
res.push_back(level); // 把当前层加入结果
}
return res;
}
};
将有序数组转换为二叉搜索树
给你一个整数数组 nums,其中元素已经按升序 排列,请你将其转换为一棵平衡二叉搜索树。平衡二叉树是指每个节点的左右两个子树的高度差的绝对值不超过 1。
分治 + 取中点。区间
[left, right]的中点作为根,递归构建左子树[left, mid-1]和右子树[mid+1, right]。取中点保证平衡。时间 O(n),空间 O(log n)。
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
TreeNode* sortedArrayToBST(vector<int>& nums) {
return build(nums, 0, nums.size() - 1);
}
private:
// 左闭右闭区间 [left, right]
TreeNode* build(vector<int>& nums, int left, int right) {
if (left > right) return nullptr; // 区间为空,返回空节点
int mid = (left + right) / 2;
TreeNode* root = new TreeNode(nums[mid]);
// 递归构建左右子树
root->left = build(nums, left, mid - 1);
root->right = build(nums, mid + 1, right);
return root;
}
};
验证二叉搜索树
给你一个二叉树的根节点 root,判断其是否是一个有效的二叉搜索树。有效 BST 定义:左子树所有节点小于根节点,右子树所有节点大于根节点,且左右子树也必须是 BST。
中序遍历检查严格递增。对 BST 中序遍历得到严格递增序列,遍历结果数组,若出现
pa[i] <= pa[i-1]则不是 BST。时间 O(n),空间 O(n)。也可用递归传上下界(min, max)做到 O(h) 空间。
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
void inorder(TreeNode *root, vector<int>& path)
{
if(root==nullptr)return;
inorder(root->left, path);
path.push_back(root->val);
inorder(root->right, path);
}
bool isValidBST(TreeNode* root) {
vector<int> pa;
inorder(root,pa);
for(int i=1;i<pa.size();i++)
{
if(pa[i]<=pa[i-1])return false;
}
return true;
}
};
二叉搜索树中第 K 小的元素
给定一个二叉搜索树的根节点 root,和一个整数 k,请你设计一个算法查找其中第 k 小的元素(从 1 开始计数)。
中序遍历取第 k 个。BST 中序遍历是升序序列,遍历到第 k 个节点即为答案。你代码用数组存全部再取
vec[k-1],时间 O(n)、空间 O(n);可优化为计数到 k 就提前返回,空间 O(h)。
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
private:
void inorder(TreeNode* node,vector<int> &vec){
if(!node) return;
inorder(node->left,vec);
vec.push_back(node->val);
inorder(node->right,vec);
}
public:
int kthSmallest(TreeNode* root, int k) {
vector<int>vec;
inorder(root,vec);
return vec[k-1];
}
};
二叉树的右视图
给定一个二叉树的根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
DFS 先右后左 + 按层记录第一个。
depth == ans.size()时说明该层还没记录过节点,当前节点就是该层最右侧节点。先递归右子树再左子树。时间 O(n),空间 O(h)。也可用 BFS 取每层最后一个。
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> ans;
void dfs(TreeNode* root, int depth) {
if (!root) return;
// 如果当前节点所在的这一层,还没有记录过节点,就记录当前节点。
if (depth == ans.size()) {
ans.push_back(root->val);
}
dfs(root->right, depth + 1);
dfs(root->left, depth + 1);
}
vector<int> rightSideView(TreeNode* root) {
dfs(root, 0);
return ans;
}
};
二叉树展开为链表
给你二叉树的根结点 root,请你将它展开为一个单链表 :展开后的单链表应该同样使用 TreeNode,其中 right 子指针指向链表中下一个结点,而 left 子指针始终为 null。展开后的单链表应该与二叉树先序遍历顺序相同。
先序遍历 + 重新连接。先序遍历收集所有节点到数组,再依次把
nodes[i-1]->right = nodes[i]、left = nullptr。时间 O(n),空间 O(n)。进阶 O(1) 空间:对每个节点,把左子树接到右子树前面(找左子树最右节点接原右子树)。
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
void flatten(TreeNode* root) {
if (!root) return;
vector<TreeNode*> nodes;
preorder(root, nodes);
for (int i = 1; i < nodes.size(); i++) {
nodes[i-1]->left = nullptr;
nodes[i-1]->right = nodes[i];
}
nodes.back()->left = nullptr;
nodes.back()->right = nullptr;
}
private:
void preorder(TreeNode* node, vector<TreeNode*>& nodes) {
if (!node) return;
nodes.push_back(node);
preorder(node->left, nodes);
preorder(node->right, nodes);
}
};
路径总和 III
给定一个二叉树的根节点 root,和一个整数 targetSum,求该二叉树里节点值之和等于 targetSum 的路径的数目。路径不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
前缀和 + 哈希表 + DFS 回溯。DFS 时维护从根到当前节点的前缀和
curSum,若curSum - target在哈希表中出现过,说明存在以当前节点结尾、和为target的路径。进入节点时prefixCount[curSum]++,离开时--(回溯)。初始prefixCount[0] = 1处理从根开始的路径。时间 O(n),空间 O(h)。
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int pathSum(TreeNode* root, int targetSum) {
unordered_map<long long, int> prefixCount;
prefixCount[0] = 1; // 前缀和为 0 的"虚拟节点",处理从根开始的路径
return dfs(root, 0, targetSum, prefixCount);
}
private:
int dfs(TreeNode* node, long long curSum, int target,
unordered_map<long long, int>& prefixCount) {
if (!node) return 0;
curSum += node->val;
// 查找是否存在前缀和为 curSum - target 的祖先
int res = prefixCount.count(curSum - target) ? prefixCount[curSum - target] : 0;
// 把当前前缀和加入哈希表,继续向下
prefixCount[curSum]++;
res += dfs(node->left, curSum, target, prefixCount);
res += dfs(node->right, curSum, target, prefixCount);
// 回溯:离开当前节点时,把它从哈希表中移除
prefixCount[curSum]--;
return res;
}
};
二叉树的最近公共祖先
给定一个二叉树,找到该树中两个指定节点的最近公共祖先。最近公共祖先定义为:对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大。
后序 DFS。递归判断左右子树是否包含 p 或 q:若左右都包含,当前节点就是 LCA;若只有一边包含,返回那一边的结果;若当前节点就是 p 或 q,返回当前节点。时间 O(n),空间 O(h)。
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
TreeNode* ans;
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
dfs(root,p,q);
return ans;
}
bool dfs(TreeNode* root,TreeNode* p,TreeNode* q)
{
if(root==nullptr) return false;
bool lson=dfs(root->left,p,q);
bool rson=dfs(root->right,p,q);
if((lson&&rson)||(root->val==p->val||root->val==q->val)&&(lson||rson))
ans=root;
//(lson&&rson):表示当前子树是否包含 p 或 q
//(root->val == p->val || root->val == q->val) && (lson || rson):表示当前节点 root 本身就是 p 或 q 之一,并且另一个目标节点位于 root 的左子树或右子树中。
return lson||rson||root->val==p->val||root->val==q->val;//表示当前子树是否包含 p 或 q
}
};
二叉树中的最大路径和
二叉树中的路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中至多出现一次。该路径至少包含一个节点,且不一定经过根节点。路径和是路径中各节点值的总和。给你一个二叉树的根节点 root,返回其最大路径和。
后序 DFS + 全局最大值。递归返回以当前节点为端点向下的最大路径和(负贡献取 0),同时用
root->val + left + right更新全局最大路径和(经过当前节点的路径)。maxn初始为INT_MIN以处理全负树。时间 O(n),空间 O(h)。
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int maxn = INT_MIN;
int maxPathSum(TreeNode* root) {
pathsum(root);
return maxn;
}
int pathsum(TreeNode* root) {
if (!root) return 0;
int left = max(0, pathsum(root->left));
int right = max(0, pathsum(root->right));
maxn = max(maxn, root->val + left + right);
return root->val + max(left, right);
}
};
8.4.2 二叉堆

cpp
#include<bits/stdc++.h>
using namespace std;
int now,tail=0;
int heap[1000001];
void insert(int x)
{
heap[++tail]=x;//从尾端入堆
int now=tail;
while(now>1)//控制不越界,1的位置是根结点
{
int fa=now/2;//父结点下标
if(heap[fa]<=heap[now]) return;//终止条件:父结点值小于等于当前位置的值,无需继续交换
swap(heap[fa],heap[now]);
now=fa;//向上继续
}
}
void de()
{
heap[1]=heap[tail--];//删除根结点,将堆结尾的数交换到根结点
now=1;
while(now*2<=tail)//控制不越界
{
int next=now*2;//next是左儿子,next+1是右儿子
if(next<tail&&heap[next+1]<heap[next]) next++;
//右儿子存在,且其值小于左二子->当前位置与右儿子比较,否则与左儿子比较
if(heap[now]<=heap[next]) return;//终止条件:当前结点值小于等于儿子结点的值,无需继续交换
swap(heap[now],heap[next]);
now=next;//向下继续
}
}
int main()
{
int n,x,y;
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&x);
if(x==1)
{
scanf("%d",&y);
insert(y);
}
if(x==2) printf("%d\n",heap[1]);
if(x==3) de();
}
}
cpp
#include<bits/stdc++.h>
using namespace std;
priority_queue<int,vector<int>,greater<int> >q;
int n,op,x;
int main()
{
cin>>n;
while(n--)
{
scanf("%d",&op);
if(op==1)
{
scanf("%d",&x);
q.push(x);
}
if(op==2) printf("%d\n",q.top());
if(op==3) q.pop();
}
return 0;
}
cpp
#include<bits/stdc++.h>
using namespace std;
int heap[1000010];
int n,op,x,k;
int main()
{
cin>>n;
while(n--)
{
scanf("%d",&op);
if(op==1)
{
scanf("%d",&x);
heap[++k]=x;
push_heap(heap+1,heap+1+k,greater<int>());
}
if(op==2) printf("%d\n",heap[1]);
if(op==3)
{
pop_heap(heap+1,heap+1+k,greater<int>());
k--;
}
}
return 0;
}
8.4.3 并查集
输入第一行为三个正整数,n、m和q。n结点数;m为给出的已知信息数量;q为查询数。接下来m行,每行2个正整数a和b,表示结点a和结点b属于同一集。接下来q行,每行2个正整数c和d,即查询结点c和d是否属于同一集。每行输入的整数以空格间隔,n、m、q均不超过1000。
输出为q+1行,前q行对应于输入的q个查询的结果,如果属于同一帮派,则输出"Yes",否则输出"No"。最后一行为一个整数,表示集的数目。
cpp
#include<bits/stdc++.h>
using namespace std;
int n,m,q;
int f[100010];
int find(int x)
{
if(f[x]==x)
return x;
else
return f[x]=find(f[x]);
}
int main()
{
cin>>n>>m>>q;
int a,b,x,y;
int ans=n-m;
for(int i=1;i<=n;i++)
f[i]=i;
while(m--)
{
cin>>a>>b;
x=find(a);
y=find(b);
if(x!=y) f[x]=y;
}
while(q--)
{
cin>>x>>y;
if(find(x)!=find(y))
cout<<"No"<<endl;
else
cout<<"Yes"<<endl;
}
cout<<ans;
return 0;
}

边带权的并查集
cpp
#include<bits/stdc++.h>
using namespace std;
int n,m;
const int N=5010;
int a[N*2],cnt=0,f1[N],d[N];
struct Node
{
int x,y;
int ans;
}node[N];
int find(int x)
{
if(f1[x]==x) return f1[x];
int root=find(f1[x]);
d[x]^=d[f1[x]];
return f1[x]=root;
}
int merge_1(int x,int y)
{
int fx1=find(x),fy1=find(y);
f1[fy1]=fx1;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
string s;
scanf("%d%d",&node[i].x,&node[i].y);
cin>>s;
if(s=="even") node[i].ans=0;
else node[i].ans=1;
a[++cnt]=node[i].x-1;
a[++cnt]=node[i].y;
}
sort(a+1,a+cnt+1);
int total=unique(a+1,a+cnt+1)-(a+1);
for(int i=1;i<=total;i++)
f1[i]=i;
for(int i=1;i<=m;i++)
{
int x=lower_bound(a+1,a+total+1,node[i].x-1)-a;
int y=lower_bound(a+1,a+total+1,node[i].y)-a;
int p=find(x),q=find(y);
if(p==q)
{
if(d[x]^d[y]!=node[i].ans)
{
cout<<i-1<<endl;
return 0;
}
}
else
{
f1[p]=q;
d[p]=d[x]^d[y]^node[i].ans;
}
}
cout<<m<<endl;
}
拓展域的并查集
cpp
#include<bits/stdc++.h>
using namespace std;
int n,m;
const int N=5010;
int a[N*2],cnt=0,f1[N],f2[N];
struct Node
{
int x,y;
int ans;
}node[N];
int find(int x)
{
if(f1[x]==x) return f1[x];
else return f1[x]=find(f1[x]);
}
int merge_1(int x,int y)
{
int fx1=find(x),fy1=find(y);
f1[fy1]=fx1;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
string s;
scanf("%d%d",&node[i].x,&node[i].y);
cin>>s;
if(s=="even") node[i].ans=0;
else node[i].ans=1;
a[++cnt]=node[i].x-1;//注意左端点对应的位置
a[++cnt]=node[i].y;
}
sort(a+1,a+cnt+1);
int total=unique(a+1,a+cnt+1)-(a+1);//离散化
for(int i=1;i<=total*2;i++)
f1[i]=i;//1~total对应奇偶性不同,total~2*total对应奇偶性相同
for(int i=1;i<=m;i++)
{
node[i].x=lower_bound(a+1,a+total+1,node[i].x-1)-a;
node[i].y=lower_bound(a+1,a+total+1,node[i].y)-a;
if(node[i].ans==0)
{
if(find(node[i].x+total)==find(node[i].y)||find(node[i].x)==find(node[i].y+total))
//此前已有条件表明左右端点奇偶性不同
{
cout<<i-1<<endl;
return 0;
}
merge_1(node[i].x,node[i].y);//合并左端点的奇数域和右端点的奇数域
merge_1(node[i].x+total,node[i].y+total);//合并左端点的偶数域和右端点的偶数域
}
else
{
if(find(node[i].x+total)==find(node[i].y+total)||find(node[i].x)==find(node[i].y))
//此前已有条件表明左右端点奇偶性相同
{
cout<<i-1<<endl;
return 0;
}
merge_1(node[i].x+total,node[i].y);//合并左端点的偶数域和右端点的奇数域
merge_1(node[i].x,node[i].y+total);//合并左端点的奇数域和右端点的偶数域
}
}
cout<<m<<endl;
}

cpp
#include<bits/stdc++.h>
using namespace std;
int n,k,cnt;
int f[50010*3];
int find(int x)
{
if(f[x]==x) return f[x];
else return f[x]=find(f[x]);
}
int main()
{
cin>>n>>k;
for(int i=1;i<=n*3;i++) f[i]=i;//1~n自身域,n~2*n捕食域,2*n~3*n被捕食域
for(int i=1;i<=k;i++)
{
int op,x,y;
scanf("%d%d%d",&op,&x,&y);
if(x>n||y>n)
{
cnt++;
continue;
}
if(op==1)//x和y是同类
{
if(find(x+n)==find(y)||find(x)==find(y+n))//y在x的捕食域或者x在y的捕食域,矛盾
cnt++;
else
{
f[find(y)]=find(x);//合并x和y的自身域
f[find(y+n)]=find(x+n);//合并x和y的捕食域
f[find(y+2*n)]=find(x+2*n); //合并x和y的被捕食域
}
}
if(op==2)//x捕食y
{
if(find(x)==find(y)||find(x)==find(y+n))//y在x的自身域或者x在y的捕食域,矛盾
cnt++;
else
{
f[find(y)]=find(x+n);//合并y的自身域和x的捕食域
f[find(y+2*n)]=find(x);//合并y的被捕食域和x的自身域
f[find(y+n)]=find(x+2*n);//合并y的捕食域和x的被捕食域
}
}
}
cout<<cnt<<endl;
return 0;
}
8.4.4 树状数组

单点修改、区间查询
cpp
#include<bits/stdc++.h>
using namespace std;
long long c[1000010];
int n;
int lowbit(int x)
{
return x&(-x);
}
void update(int j,int x)
{
for(int i=j;i<=n;i+=lowbit(i))
{
c[i]+=x;
}
}
long long get_sum(int add)
{
long long ans=0;
for(int i=add;i;i-=lowbit(i))
{
ans+=c[i];
}
return ans;
int main()
{
int n,q,i;
cin>>n>>q;
for(i=1;i<=n;i++)
{
int a;
scanf("%d",&a);
update(i,a);
}
while(q--)
{
int op;
cin>>op;
if(op==1)
{
int j,x;
cin>>j>>x;
update(j,n,x);
}
if(op==2)
{
int l,r;
cin>>l>>r;
long long sum;
sum=get_sum(r)-get_sum(l-1);
cout<<sum<<endl;
}
}
return 0;
}
树状数组+差分
区间修改、单点查询
cpp
#include<bits/stdc++.h>
using namespace std;
long long a[500005],dif[500005],c[500005];
int n,m;
int lowbit(int x)
{
return x&(-x);
}
void update(int x,long long p)
{
for(int i=x;i<=n;i+=lowbit(i))
c[i]+=p;
}
long long get_sum(int x)
{
long long sum=0;
for(int i=x;i;i-=lowbit(i))
sum+=c[i];
return sum;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
scanf("%lld",&a[i]);
while(m--)
{
int option;
scanf("%d",&option);
if(option==1)
{
int l,r;
long long z;
scanf("%d%d%lld",&l,r,&z);
update(l,z);
update(r+1,-z);
}
if(option==2)
{
int q;
scanf("%d",&q);
cout<<get_sum(q)+a[q]<<endl;
}
}
return 0;
}
树状数组求逆序对
任意给定一个集合a,用tval保存数值val1在集合a内出现的次数,那么数组t在l,r上的区间和 ∑ i = 1 r t i \sum_{i=1} ^{r} ti ∑i=1rti就表示集合a中范围在l,r内的数有多少个。可以在集合a的数值范围上建立一个树状数组来维护t的前缀和。
对于给定的序列a,倒序扫描之,对于每个数ai:
- 在树状数组中查询前缀和1,a\[i-1],累加到答案ans中(这些数比ai小,位置却在它之后)。
- 单点修改,ta\[i]++,同时维护前缀和。
数值范围较大时可以先离散化。
cpp
#include<bits/stdc++.h>
using namespace std;
const int N=200010;
int n,pos[N],love[N],seat[N],c[N],ans[N];
int lowbit(int x)
{
return x&(-x);
}
void add(int p)
{
for(int i=p;i<=n;i+=lowbit(i))
c[i]++;
}
int get(int p)
{
int sum=0;
for(int i=p;i;i-=lowbit(i))
sum+=c[i];
return sum;
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
int x;
scanf("%d%d",&x,&love[i]);
pos[x]=i;
}
for(int i=1;i<=n;i++)
seat[i]=pos[love[i]];
for(int i=n;i;i--)
{
ans[i]+=get(seat[i]);
add(seat[i]);
}
memset(c,0,sizeof(c));
for(int i=1;i<=n;i++)
{
ans[i]+=get(n)-get(seat[i]);
add(seat[i]);
printf("%d\n",ans[i]);
}
return 0;
}
9.4.5 字典树
Trie 是一种用于实现字符串快速检索的多叉树结构。Trie的每个结点都拥有若干个字符指针,若在插入或检索字符串时扫描到一个字符c,就沿着当前节点的c字符指针,走向该指针的结点。

看题目的描述很容易联想到当时学的归并排序求逆序对数,但是这里并不是一些有大小顺序的数而是很多字符串,怎么办呢?
f ( s t r i n g ) − > n u m b e r f(string)->number f(string)−>number
我们可以采取很多种映射的办法,最直接的是map,其他的一些比如哈希等,这里采用字典树,插入时走到字符串末尾时给每个字符串一个编号,重排之后原有的顺序被打乱,归并排序的时候顺便求一下逆序对数就好啦。
cpp
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
int n,tot,trie[N][100],a[N],b[N],temp[N];
long long ans;
void insert(string s,int x)
{
int p=0,len=s.size();
for(int i=0;i<len;i++)
{
if(!trie[p][s[i]-'A'])
trie[p][s[i]-'A']=++tot;
p=trie[p][s[i]-'A'];
}
a[p]=x;
}
void query(string s,int x)
{
int p=0,len=s.size();
for(int i=0;i<len;i++)
p=trie[p][s[i]-'A'];
b[x]=a[p];
}
void merge_sort(int l,int r)
{
if(l==r) return;
int mid=(l+r)>>1;
merge_sort(l,mid);
merge_sort(mid+1,r);
int i=l,j=mid+1,k=l;
while(i<=mid&&j<=r)
{
if(b[i]<=b[j]) temp[k++]=b[i++];
else temp[k++]=b[j++],ans+=mid-i+1;
}
while(i<=mid) temp[k++]=b[i++];
while(j<=r) temp[k++]=b[j++];
for(int p=l;p<=r;p++) b[p]=temp[p];
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
string name;
cin>>name;
insert(name,i);
}
for(int i=1;i<=n;i++)
{
string name;
cin>>name;
query(name,i);
}
//for(int i=1;i<=n;i++)
//cout<<b[i]<<' ';
merge_sort(1,n);
cout<<ans;
return 0;
}

暴力的做法是对于每一个数,枚举其他所有数,求出(n-1)个异或值,有了字典树之后可以快速找到跟当前数异或值最大的那个。把这些01串都插入字典树,然后枚举这n个数,在这n个数对应找出的异或值中选最大的就行了。
怎么找到一个数对应的最大异或值呢?从高位(树根)开始每次都进入反子树(比如101011,第一次进入0子树,第二次进入1子树,第三次进入0子树,以此类推),如果想要进入的子树为空的话就迫不得已进入同子树。这样可以尽可能地选取到异或值大的数。每向下移一层,异或值乘2,如果进入反子树异或值加1。
cpp
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,trie[N*32][2],tot,a[N];
int maxn=0;
inline int read()
{
int 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-48;ch=getchar();}
return x*f;
}
void insert(int x)
{
int p=0;
for(int i=31;i>=0;i--)
{
int t=(x>>i)&1;
if(!trie[p][t])
trie[p][t]=++tot;
p=trie[p][t];
}
}
int query(int x)
{
int q=0,ans=0,t;
for(int i=31;i>=0;i--)
{
t=(x>>i)&1;//取出当前位
if(!trie[q][!t])//反子树为空
{
q=trie[q][t];
ans<<=1;
}
else
{
q=trie[q][!t];//进入反子树
ans<<=1;
ans+=1;//0^1=1
}
}
return ans;
}
int main()
{
n=read();
for(int i=0;i<n;i++)
{
a[i]=read();
insert(a[i]);
}
for(int i=0;i<n;i++)
{
int temp=query(a[i]);
maxn=max(maxn,temp);
}
cout<<maxn;
return 0;
}
实现 Trie (前缀树)
Trie(发音类似 "try")或者说前缀树是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。请你实现 Trie 类:insert(word) 向前缀树中插入字符串;search(word) 如果字符串在前缀树中返回 true;startsWith(prefix) 如果前缀树中存在以 prefix 为前缀的字符串返回 true。
多叉树 + 字母映射。每个节点有 26 个孩子指针和一个
isEnd标记;insert逐字符向下建节点,末尾标记isEnd;search逐字符向下走,走到末尾且isEnd为 true 才算找到;startsWith逐字符走通即可,不要求isEnd。时间 O(L)(L 为字符串长度),空间 O(总字符数)。
cpp
class Trie {
struct Node {
Node* ch[26]{};
bool isEnd = false;
};
Node* root;
public:
Trie() {
root = new Node();
}
void insert(string word) {
Node* p = root;
for (char c : word) {
int i = c - 'a';
if (!p->ch[i]) p->ch[i] = new Node();
p = p->ch[i];
}
p->isEnd = true;
}
bool search(string word) {
Node* p = root;
for (char c : word) {
int i = c - 'a';
if (!p->ch[i]) return false;
p = p->ch[i];
}
return p->isEnd;
}
bool startsWith(string prefix) {
Node* p = root;
for (char c : prefix) {
int i = c - 'a';
if (!p->ch[i]) return false;
p = p->ch[i];
}
return true;
}
};
/**
* Your Trie object will be instantiated and called as such:
* Trie* obj = new Trie();
* obj->insert(word);
* bool param_2 = obj->search(word);
* bool param_3 = obj->startsWith(prefix);
*/
8.5 图
图的创建
请编写程序创建一个有向图。有向图中包含n个顶点,编号为0至n-1。
输入格式:
输入第一行为两个正整数n和e,分别表示图的顶点数和边数,其中n不超过20000,e不超过20000。接下来e行表示每条边的信息,每行为3个非负整数a、b、c,其中a和b表示该边的端点编号,c表示权值。各边并非按端点编号顺序排列。
输出格式:
按顶点编号递增顺序输出每个顶点引出的边,每个顶点占一行,若某顶点没有引出边,则不输出。每行表示一个顶点引出的所有边,格式为a:(a,b,w)......,表示有向边a->b的权值为w,a引出的多条边按编号b的递增序排列。
cpp
#include<bits/stdc++.h>
using namespace std;
vector<int>v[20010];
int m,adj[20010],temp[20010],mem[20010];
struct edge
{
int to;
int next;
int w;
}e[20010];
void add(int x,int y,int z)
{
++m;
v[x].push_back(m);
e[m].to=y;
e[m].next=x;
e[m].w=z;
}
int main()
{
int n,ed,i;
cin>>n>>ed;
for(i=1;i<=ed;i++)
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
}
//for(i=0;i<n;i++)
//cout<<i<<' '<<v[i].size()<<endl;
for(i=0;i<n;i++)
{
memset(temp,0,sizeof(temp));
memset(mem,0,sizeof(mem));
if(v[i].size())
{
int cap=v[i].size();
for(int j=0;j<cap;j++)
{
temp[j]=e[v[i][j]].to;
mem[temp[j]]=e[v[i][j]].w;
}
sort(temp,temp+cap);
cout<<i<<':';
for(int j=0;j<cap;j++)
cout<<"("<<i<<","<<temp[j]<<","<<mem[temp[j]]<<")";
cout<<endl;
}
}
return 0;
}
岛屿数量
给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
DFS 遍历 + 访问标记。遍历网格,遇到
'1'且未访问时,岛屿数 +1,并从该点 DFS 把所有相连的陆地标记为已访问。时间 O(mn),空间 O(mn)(可原地把'1'改成'0'省掉 vis 数组)。
cpp
class Solution {
public:
int cnt = 0;
int vis[350][350] = {0}; // 显式初始化,更安全
int numIslands(vector<vector<char>>& grid) {
int m = grid.size(), n = grid[0].size();
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// 关键:必须是陆地,且未访问
if (grid[i][j] == '1' && !vis[i][j]) {
dfs(grid, i, j);
cnt++;
}
}
}
return cnt; // 关键:要有返回值
}
private:
void dfs(vector<vector<char>>& grid, int i, int j) {
int m = grid.size(), n = grid[0].size();
// 越界 或 不是陆地 或 已访问,都返回
if (i < 0 || i >= m || j < 0 || j >= n) return;
if (grid[i][j] != '1' || vis[i][j]) return;
vis[i][j] = 1;
dfs(grid, i - 1, j);
dfs(grid, i + 1, j);
dfs(grid, i, j - 1);
dfs(grid, i, j + 1);
}
};
8.6 分块

cpp
#include<bits/stdc++.h>
using namespace std;
const int N=100010;
long long a[N],sum[N],add[N];
int l[N],r[N],pos[N];
int n,m,t;
void change(int L,int R,long long plus)
{
int p=pos[L],q=pos[R];
if(p==q)
{
for(int i=L;i<=R;i++)
a[i]+=plus;
sum[p]+=plus*(R-L+1);
}
else
{
for(int i=p+1;i<=q-1;i++)
add[i]+=plus;
for(int i=L;i<=r[p];i++)
a[i]+=plus;
sum[p]+=(r[p]-L+1)*plus;
for(int i=l[q];i<=R;i++)
a[i]+=plus;
sum[q]+=(R-l[q]+1)*plus;
}
}
long long ask(int L,int R)
{
long long ans=0;
int p=pos[L],q=pos[R];
if(p==q)
{
for(int i=L;i<=R;i++)
ans+=a[i];
ans+=add[p]*(R-L+1);
}
else
{
for(int i=p+1;i<=q-1;i++)
ans+=sum[i]+add[i]*(r[i]-l[i]+1);
for(int i=L;i<=r[p];i++)
ans+=a[i];
ans+=add[p]*(r[p]-L+1);
for(int i=l[q];i<=R;i++)
ans+=a[i];
ans+=add[q]*(R-l[q]+1);
}
return ans;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
scanf("%lld",&a[i]);
t=sqrt(n);
for(int i=1;i<=t;i++)
{
l[i]=(i-1)*sqrt(n)+1;
r[i]=i*sqrt(n);
}
if(r[t]<n)
{
t++;
l[t]=r[t-1]+1;
r[t]=n;
}
for(int i=1;i<=t;i++)
{
for(int j=l[i];j<=r[i];j++)
{
pos[j]=i;
sum[i]+=a[j];
}
}
while(m--)
{
char op[3];
int L,R;
long long plus;
scanf("%s%d%d",op,&L,&R);
if(op[0]=='C')
{
scanf("%lld",&plus);
change(L,R,plus);
}
else printf("%lld\n",ask(L,R));
}
return 0;
}
9.图论
9.1 最小生成树
9.1.1 Prim算法
cpp
#include<bits/stdc++.h>
using namespace std;
#define inf 0x7fffffff
int n,m,sum;
int dis[5005],vis[5005];
struct Edge
{
int des,w;
}ed1,ed2;
vector<Edge>vec[5005];
int prim()
{
for(int i=2;i<=n;i++)
dis[i]=inf;//初始化:各点到树的距离为无穷大
int now=1;//默认从1开始建树
for(int i=0;i<vec[1].size();i++)
dis[vec[1][i].des]=min(vec[1][i].w,dis[vec[1][i].des]);
//更新:此时树中有1个元素1,于是各点到树的距离即为到1的距离
for(int i=2;i<=n;i++)
{
vis[now]=1;
int minn=inf;
int index=0;
for(int j=2;j<=n;j++)
{
if(dis[j]<minn&&!vis[j])
{
minn=dis[j];
index=j;
}
}//遍历未被访问的各点,找到距离树最近的点
if(!index) return -1;//如果各点到树的距离均为无穷大,无法生成MST,返回-1
sum+=minn;//加上路径
now=index;//新的元素加入树
for(int j=0;j<vec[now].size();j++)
{
if(vec[now][j].w<dis[vec[now][j].des])
dis[vec[now][j].des]=vec[now][j].w;
//遍历新的元素和其余各点的距离,更新各点到树的最小距离
}
}
return sum;
}
int main()
{
cin>>n>>m;
for(int i=0;i<m;i++)
{
int a,b,len;
scanf("%d%d%d",&a,&b,&len);
ed1.des=a,ed2.des=b,ed1.w=ed2.w=len;
vec[a].push_back(ed2);
vec[b].push_back(ed1);
}
int ans=prim();
if(ans==-1) cout<<"There is no minimum spanning tree";
else cout<<ans;
return 0;
}
9.1.2 Kruskal算法
cpp
#include<bits/stdc++.h>
using namespace std;
int n,e,i;
int F[1501];
struct Edge
{
int from;
int to;
int w;
}edge[1501];
bool cmp(Edge x,Edge y)
{
return x.w<y.w;
}
int find(int x)
{
if(F[x]==x) return F[x];
else return F[x]=find(F[x]);
}
bool fun(int a,int b)
{
int x=find(a);
int y=find(b);
if(x!=y)
{
F[y]=x;
return true;
}
return false;
}
int main()
{
scanf("%d%d",&n,&e)
int total=0,cnt=0;
for(i=0;i<e;i++)
scanf("%d%d%d",&edge[i].from,&edge[i].to,&edge[i].w);
for(i=0;i<n;i++) F[i]=i;
sort(edge,edge+e,cmp);
for(i=0;i<e;i++)
{
if(fun(edge[i].from,edge[i].to))
{
total+=edge[i].w;
cnt++;
}
if(cnt==n-1) break;
}
if(cnt==n-1) printf("%d\n",total);
else cout<<"There is no minimum spanning tree."<<endl;
return 0;
}
9.2 拓扑排序
设 G为有 n 个顶点的带权有向无环图,G 中各顶点的编号为 1 到 n,请设计算法,计算图 G 中 1, n 间的最长路径。
cpp
//45ms / 3.10MB / 892B C++14 (GCC 9) O2
#include<bits/stdc++.h>
using namespace std;
queue<int>q;
int n,m;
int maxn[1510],mark[1510],ind[1510];
//maxn:到当前结点之前的最长路
//mark:标记"在路径上"
//ind:记录每个结点的入度
struct Edge
{
int from;
int to;
int w;
}edge[50010];
vector<Edge>vec[50010];//vec为Edge型的,在已知起点时可直接通过这个调用出边权和终点
void topo_sort()//拓扑排序
{
for (int i=1;i<=n;i++)
if (ind[i]==0)
q.push(i);//先将所有入度为0 的结点放入队列
while(q.size())
{
int now=q.front();//每次取出队首元素
q.pop();
for(int j=0;j<vec[now].size();j++)//遍历队首元素指向的所有结点
{
ind[vec[now][j].to]--;//去掉当前结点的同时,它指向的所有结点入度减1
if(mark[now])//在路径上
{
if(maxn[vec[now][j].to]<maxn[now]+vec[now][j].w)
maxn[vec[now][j].to]=maxn[now]+vec[now][j].w;
//如果from之前的最长路加上边权大于之前获得的to之前的最长路,就更新一下
mark[vec[now][j].to]=1;//标记终点在路径上
}
if(!ind[vec[now][j].to]) //删边操作之后终点的入度变为0,则将其压入队列
q.push(vec[now][j].to);
}
}
}
int main()
{
int i;
cin>>n>>m;
for(i=0;i<m;i++)
{
scanf("%d%d%d",&edge[i].from,&edge[i].to,&edge[i].w);
vec[edge[i].from].push_back(edge[i]);
ind[edge[i].to]++;
}
maxn[n]=-1;
mark[1]=1;//标记起点(1)在路径上
topo_sort();
cout<<maxn[n];
return 0;
}
课程表
你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1。在选修某些课程之前需要一些先修课程,先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [a_i, b_i] 表示如果要学习课程 a_i 则必须先学习课程 b_i。请你判断是否可能完成所有课程的学习。
拓扑排序(BFS + 入度)。构建邻接表和入度数组,所有入度为 0 的课程入队;依次出队并"删除"其出边(入度减 1),入度变 0 的入队;最后判断访问过的课程数是否等于
numCourses。时间 O(V+E),空间 O(V+E)。
cpp
class Solution {
private:
vector<vector<int>> edges;
vector<int> indeg;
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
edges.resize(numCourses);
indeg.resize(numCourses);
for (const auto& info: prerequisites) {
edges[info[1]].push_back(info[0]);
++indeg[info[0]];
}
queue<int> q;
for (int i = 0; i < numCourses; ++i) {
if (indeg[i] == 0) {
q.push(i);
}
}
int visited = 0;
while (!q.empty()) {
++visited;
int u = q.front();
q.pop();
for (int v: edges[u]) {
--indeg[v];
if (indeg[v] == 0) {
q.push(v);
}
}
}
return visited == numCourses;
}
};
9.3 最短路
9.3.1 Dijkstra算法
cpp
#include<bits/stdc++.h>
using namespace std;
#define INF 0x3ffffff
struct Node
{
int v,l;//一条路的尾结点,该路的长度
};
int n,e,from,to;
vector<Node> v[1001];
int dis[1001];
int path[1001];
int visit[1001];
void Dijkstra(int from)
{
fill(dis,dis + 1001,INF);
dis[from] = 0;
for(int i = 0;i < n;i ++)
{
int u = -1,minx = INF;
for(int j = 0;j < n;j ++)
{
if(visit[j] == 0 && dis[j] < minx)
{
minx = dis[j];
u = j;
}
}
if(u == -1) break;
visit[u] = 1;
for (int j = 0; j < v[u].size(); j++)
{
int x = v[u][j].v;
if (visit[x] == 0 && dis[u] + v[u][j].l < dis[x])
{
dis[x] = dis[u] + v[u][j].l;
path[x] = u;
}
}
}
}
int main()
{
cin >> n >> e;
for (int i = 0; i < e / 2; i++)
{
int a, b, c;
cin >> a >> b >> c;
v[a].push_back(Node{b, c});
v[b].push_back(Node{a, c});
}
cin >> from >> to;
if (from == to)
{
printf("%d-->%d:0",from,from);
return 0;
}
Dijkstra(from);
vector<int> ve;
int x = to;
while (x != from)
{
ve.push_back(x);
x = path[x];
}
cout << from;
for (int i = ve.size() - 1; i >= 0; i--)
cout << "-->" << ve[i];
cout << ":" << dis[to];
}
单调队列优化:
将常规版本的Dijkstra 中每个结点遍历所有相邻结点找到距离最近的这一步用堆优化,每次直接从小根堆中直接取出顶点,可以将时间复杂度降到O(VlogV)。
cpp
// 309ms / 7.68MB / 1.02KB C++14 (GCC 9) O2
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,m,s;
int dis[N],vis[N];
struct Node
{
int to,w;
bool operator < (const Node& x) const
{
return w>x.w;
}
};
vector<Node> vec[N];
priority_queue<Node>q;
//默认大根堆(从头取数据从大到小),运算符重载后现在为小根堆
void Dijkstra()
{
memset(dis,0x3f,sizeof(dis));//距离初始化为无穷大
dis[s]=0;
Node now;
now.to=s,now.w=0;
q.push(now);//起点入队
while(!q.empty())
{
int next=q.top().to;//取出队顶元素(距离最小的)
q.pop();
if(vis[next]) continue;//已经访问过了就下一个
vis[next]=1;//标记已经访问过
for(int i=0;i<vec[next].size();i++)//遍历当前结点相邻的所有结点
{
if(vec[next][i].w+dis[next]<dis[vec[next][i].to])//松弛操作
{
dis[vec[next][i].to]=vec[next][i].w+dis[next];
now.to=vec[next][i].to;
now.w=dis[vec[next][i].to];
q.push(now);
}
}
}
}
int main()
{
scanf("%d%d%d",&n,&m,&s);
for(int i=1;i<=m;i++)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
Node no;
no.to=b;
no.w=c;
vec[a].push_back(no);
}
Dijkstra();
for(int i=1;i<=n;i++)
printf("%d ",dis[i]);
return 0;
}
9.3.2 SPFA算法
给出一个N个顶点M条边的无向无权图,顶点编号为1-N。问从顶点1开始,到其他每个点的最短路有几条。
cpp
// 1:50ms / 3.21MB / 863B C++14 (GCC 9) O2
// 2:48ms / 3.13MB / 877B C++14 (GCC 9) O2
#include<bits/stdc++.h>
using namespace std;
const int mod=100003,N=1000010;
int n,m,ans[N],minn[N],vis[N];
struct edge
{
int a,b;
}e[N*2];
vector<int>vec[N];
queue<int>q;
inline int read()
{
int 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-48;ch=getchar();}
return x*f;
}
void SPFA(int start)
{
minn[start]=0,ans[start]=1,vis[start]=1;
q.push(start);
int now;
while(q.size())
{
now=q.front();
q.pop();
for(int i=0;i<vec[now].size();i++)
{
if(!vis[vec[now][i]])
{
vis[vec[now][i]]=1;
if(minn[vec[now][i]]>minn[now]+1)
minn[vec[now][i]]=minn[now]+1;
q.push(vec[now][i]);
}
if(minn[vec[now][i]]==minn[now]+1)
ans[vec[now][i]]=(ans[vec[now][i]]+ans[now])%mod;
}
}
}
int main()
{
memset(minn,127,sizeof(minn));
n=read(),m=read();
for(int i=0;i<m;i++)
{
int x,y;
x=read(),y=read();
if(x==y) continue;
vec[x].push_back(y);
vec[y].push_back(x);
}
SPFA(1);
for(int i=1;i<=n;i++)
printf("%d\n",ans[i]);
return 0;
}
10.3.3 Floyd算法
cpp
#include<bits/stdc++.h>
using namespace std;
#define N 1000
int n,m,x,y,z,posx=-1,posy=-1,max1;
int maps[N][N];
int path[N][N];
int pre[N];
void f(int x,int y)
{
int k=path[x][y];
if(k==-1){//找不到更近的
pre[y]=x;
return;
}
f(k,y);
f(x,k);
}
void print(int x,int y)
{
if(x==y)
{
cout<<x;
return;
}
print(x,pre[y]);
cout<<"->"<<y;
return;
}
int main()
{
memset(path,-1,sizeof(path));
cin>>n>>m;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
if(i!=j) maps[i][j]=0x3ffffff;
for(int i=1;i<=m;i++)
{
cin>>x>>y>>z;
if(maps[x][y]>z) maps[x][y]=z;
}
for(int k=0;k<n;k++)
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
if(maps[i][j]>maps[i][k]+maps[k][j])
{
maps[i][j]=maps[i][k]+maps[k][j];
path[i][j]=k;
}
int t=2;
while(t--)
{
cin>>x>>y;
if(x==y) cout<<x<<"->"<<y<<":0"<<endl;
else
{
if(maps[x][y]==0x3ffffff)
cout<<x<<"->"<<y<<":-1"<<endl;
else
{
memset(pre,0,sizeof(pre));
f(x,y);
print(x,y);
cout<<":"<<maps[x][y]<<endl;
}
}
}
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
if(maps[i][j]!=0x3ffffff&&maps[i][j]>max1)
{
max1=maps[i][j];
posx=i;
posy=j;
}
memset(pre,0,sizeof(pre));
if(posx!=-1&&posy!=-1)
{
f(posx,posy);
print(posx,posy);
cout<<":"<<maps[posx][posy]<<endl;
}
else cout<<endl;
}
9.4 负环
负环判断

cpp
#include<bits/stdc++.h>
using namespace std;
const int N=10010;
int T,n,m;
int head[N],ver[N],nex[N],tot,vis[N],cnt[N],val[N],dis[N];
queue<int>q;
void add(int x,int y,int z)
{
ver[++tot]=y,nex[tot]=head[x],head[x]=tot,val[tot]=z;
if(z>=0) ver[++tot]=x,nex[tot]=head[y],head[y]=tot,val[tot]=z;
}
void init()
{
tot=0;
memset(head,0,sizeof(head));
memset(ver,0,sizeof(ver));
memset(nex,0,sizeof(nex));
memset(vis,0,sizeof(vis));
memset(val,0,sizeof(val));
memset(cnt,0,sizeof(cnt));
memset(dis,0x3f,sizeof(dis));
while(!q.empty()) q.pop();
}
bool spfa()
{
vis[1]=1,dis[1]=0;
q.push(1);
while(q.size())
{
int now=q.front();
q.pop();
vis[now]=0;
for(int i=head[now];i;i=nex[i])
{
int to=ver[i];
if(dis[now]+val[i]<dis[to])
{
dis[to]=dis[now]+val[i];
if(!vis[to])
{
cnt[to]=cnt[now]+1;
if(cnt[to]>=n) return true;
vis[to]=1,q.push(to);
}
}
}
}
return false;
}
int main()
{
cin>>T;
while(T--)
{
init();
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
}
if(spfa()) cout<<"YES"<<endl;
else cout<<"NO"<<endl;
}
}
9.5 二分图

cpp
#include<bits/stdc++.h>
using namespace std;
const int N=520;
int n,m,e,tot,ans;
int mp[N][N],vis[N],match[N];
bool dfs(int x)
{
for(int i=1;i<=m;i++)
{
if(!mp[x][i]||vis[i]) continue;
vis[i]=1;
if(!match[i]||dfs(match[i]))
{
match[i]=x;
return true;
}
}
return false;
}
int main()
{
cin>>n>>m>>e;
for(int i=1;i<=e;i++)
{
int x,y;
scanf("%d%d",&x,&y);
mp[x][y]=1;
}
for(int i=1;i<=n;i++)
{
memset(vis,0,sizeof(vis));
if(dfs(i)) ans++;
}
cout<<ans;
}
9.6 欧拉路径
欧拉路径:欧拉路是指从图中任意一个点开始到图中任意一个点结束的路径,并且图中每条边通过的且只通过一次。
欧拉回路:欧拉回路是指起点和终点相同的欧拉路。
对于无向图,所有边都是连通的
(1)存在欧拉路径的充分必要条件:度数为奇数的点只能是0个或者2个
(2)存在欧拉回路的充分必要条件:度数为奇数的只能是0个
2对于有向图,所有边都是连通的
(1)存在欧拉路径的充分必要条件:要么所有点的出度均等于入度, 要么除了两个点之外,其余所有点的出度等于入度,剩余的两个点:一个满足出度比入度多1(起点),另一个满足入度比出度多1(终点)
(2)存在欧拉回路的充分必要条件:所有点的出度均等于入度
求有向图字典序最小的欧拉路径。
cpp
#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int n,m,start=1;
int ind[N],outd[N],p[N];
vector<int>vec[N];
stack<int>st;
void dfs(int now)
{
for(int i=p[now];p[now]<vec[now].size();i++)
{
p[now]++;
//cout<<"p[now]:"<<p[now]<<' '<<vec[now].size()<<endl;
dfs(vec[now][i]);
}
st.push(now);
}
int main()
{
cin>>n>>m;
for(int i=0;i<m;i++)
{
int from,to;
scanf("%d%d",&from,&to);
vec[from].push_back(to);
ind[to]++;
outd[from]++;
}
int flag=0;
for(int i=1;i<=n;i++)
{
sort(vec[i].begin(),vec[i].end());
//cout<<i<<' '<<ind[i]<<' '<<outd[i]<<endl;
if(ind[i]==outd[i]) continue;
//cout<<i<<' '<<ind[i]<<' '<<outd[i]<<endl;
else if(ind[i]==outd[i]+1) flag++;
else if(ind[i]==outd[i]-1) start=i,flag--;
else
{
cout<<"No";
return 0;
}
}
if(flag!=0)
{
cout<<"No";
return 0;
}
dfs(start);
while(st.size())
cout<<st.top()<<' ',st.pop();
return 0;
}