PAT:知识点总结

总结

    • 1 数学篇
      • 1.1 最大公约数
      • 1.1 最小公倍数
      • 1.2 素数
      • 1.3 分解质因子
      • 1.4 分数四则运算
      • 1.5 大整数四则运算
        • 一、用结构体bign实现:
        • 二、用string存储
    • 2. 数据结构篇
      • 2.1 线性表:
        • 处理思路 1:排序+筛选
        • 处理思路 2:下标(地址)排序
      • 2.2 树:
        • 树的遍历:
        • 小技巧
        • 建树:
          • 处理思路 1: 先建树再对树遍历
          • 处理思路 2: 直接递归遍历
        • 二叉查找树 BST
          • 插入:
          • 删除
        • AVL
          • 插入:
          • 删除
        • Heap
        • 哈夫曼树
        • LCA
        • 并查集(Disjoint-set)
          • 版本1:father[i] = i;
          • 版本2:father[i] = -1;
      • 2.3 图
        • 顶点处理
        • 图的遍历
        • 最短路径
          • Dijkstra
          • BF
          • SPFA
          • Floyd
        • 最小生成树
          • prim
          • kruskal
        • 拓扑排序
        • 关键路径

1 数学篇

1.1 最大公约数

  • 辗转相除法
int gcd(int a, int b)
{
	return b == 0 ? a : gcd(b, a % b);
}
  • 示例:gcd(4, 6) = 2;

1.1 最小公倍数

a,b的最小公倍数 = a * b / gcd(a, b); ,但 a * b 可能超范围故用:a / gcd(a, b) * b

int lcm(int a, int b)
{
	return a / gcd(a, b) * b;
}
  • 示例: lcm(3, 4) = 12;

1.2 素数

  • TIPS 1 :

素数:只能整除1和它本身的数(只有1、本身两个正因子)

边界条件: 0 ,1

  • 定义法:isPrime()

对一个数n:若它能整数:2~sqrt(n)之间的任一数,就是合数,反之为素数

bool isPrime(int n){
	if(n <= 1) return false;
	int sqr = sqrt(1.0*n);
	for(int i = 2; i <= sqr; ++i){
		if(n % i == 0) return false;
	} 
	return true;
}
void FindPrime(int n)
{
	for(int i = 1; i <= n; ++i){
		if(isPrime(i) == true){
			prime.push_back(i);
			p[i] = true;
		}
	}
}
  • Eratoshenes筛法:GetPrime()

从2开始(10 既不是素数也不是合数),X掉(置为 true )所有素数的倍数(合数),剩下的数就是素数:

bool p[101];    //素数标记表要开打点防止越界产生 段错误
vector<int> prime;
void getPrime(int n){
	for(int i = 2; i <= n; ++i){
		if(p[i] == false){
			prime.push_back(i);
			for(int j = i + i; j <= n; j += i){
				p[j] = true;
			}
		}
	}
}

100以内的素数表:
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97

1.3 分解质因子

TIPS1:

规则:一个数n不可能存在2个 大于 srqt(n)的因子

步骤:
step 1:GetPrime(sqrt(n));
step 2: 枚举这些prime,如果是因子(if(n % prime[i] == 0))求出该质因子个数:while(n % prime[i]) n /= prime[i];
step 3: 循环结束后若 n != 1 ,说明有比sqrt(n)大的质因子,加入fac

struct Fac
{
	int f, cnt;
}fac[20];
GetPrime((int)sqrt(n));
for(int i = 0; i < prime.size(); ++i)
{
	int now = prime[i];
	if(n % now == 0)
	{
		fac[idex].f = now;
		while(n % now == 0)
		{
			fac[idex].cnt++;
			n /= now;
		}
		idex++;
	}
}
if(n != 1)
{
	fac[idex].f = n;
	fac[idex++].cnt = 1;
}

1.4 分数四则运算

  • TIPS1:

化简规则: 分母:始终为正,分子为0时取1; 约分除gcd
输出规则:分数分为 整数, 假分数, 真分数

  • TIPS2:

乘法可能会超范围,使用long long 存分子分母

  • 化简
int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); }
Frac Reduce(Frac ans)
{
    if(ans.down < 0)
    {
        ans.up = -ans.up;
        ans.down = -ans.down;
    }
    if(ans.up == 0)
    {
        ans.down = 1;
    }else
    {
        int d = gcd(abs(ans.up), ans.down);
        ans.up /= d;
        ans.down /= d;
    }
    return ans;
}
  • 输出:
void ShowAns(Frac ans)
{
    ans = Reduce(ans);
    if(ans.down == 1)
    {
        printf("%d", ans.up);
    }else if(abs(ans.up) > ans.down)
    {
        printf("%d %d/%d", ans.up / ans.down, abs(ans.up) % ans.down, ans.down);
    }else
    {
        printf("%d/%d", ans.up, ans.down);
    }
}
  • 补充:

对于除数非法:补充一个标记位:

struct Frac
{
    ll up, down;
    bool inf;
    Frac() { inf = false; }
};
Frac Divid(Frac a, Frac b)
{
    Frac ans;
    if(b.up == 0) ans.inf = true;
    ans.up = a.up * b.down;
    ans.down = a.down * b.up;
    return Reduce(ans);
}
  • 完整版:ShowAns();
void ShowAns(Frac a)
{
    a = Reduce(a);
    if(a.inf) printf("Inf");
    else
    {
        if(a.up < 0) printf("(");
        ...
        if(a.up < 0) printf(")");
    }
}

1.5 大整数四则运算

一、用结构体bign实现:

  • 存储方式
const int maxn = 1010;
struct Bign
{
    int num[maxn];
    int len;
    Bign()
    {
        len = 0;
        memset(num, 0, sizeof(num));
    }
};
  • 输入
Bign Change(string n)
{
    Bign ans;
    ans.len = n.size();
    for(int i = 0; i < n.size(); ++i)
    {
        ans.num[n.size()-1-i] = n[i] - '0';
    }
    return ans;
}
  • 比较
int BignCmp(Bign a, Bign b)
{
    if(a.len > b.len) return 1;
    else if(a.len < b.len) return -1;
    else
    {
        for(int i = a.len - 1; i >= 0; --i)
        {
            if(a.num[i] > b.num[i]) return 1;
            else if(a.num[i] < b.num[i]) return -1;
        }
        return 0;
    }
}
  • 输出:
void ShowBign(Bign a)
{
    for(int i = a.len-1; i >= 0; --i) printf("%d", a.num[i]);
}
  • 加法:
Bign Add(Bign a, Bign b)
{
    Bign ans;
    int carry = 0;
    for(int i = 0; i < a.len || i < b.len; ++i)
    {
        int tmp = a.num[i] + b.num[i] + carry;
        ans.num[ans.len++] = tmp % 10;
        carry = tmp / 10;
    }
    if(carry != 0) ans.num[ans.len++] = carry;
    return ans;
}
  • 乘法:
Bign Prod(Bign a, int b)
{
    Bign ans;
    int carry = 0;
    for(int i = 0; i < a.len; ++i)
    {
        int tmp = a.num[i] * b + carry;
        ans.num[ans.len++] = tmp % 10;
        carry = tmp / 10;
    }
    while(carry)
    {
        ans.num[ans.len++] = carry % 10;
        carry /= 10;
    }
    return ans;
}
  • 减法:
Bign Minus(Bign a, Bign b)
{
    if(BignCmp(a, b) < 0)
    {
        printf("-");
        swap(a, b);
    }
    Bign ans;
    for(int i = 0; i < a.len; ++i)
    {
        if(a.num[i] < b.num[i])
        {
            a.num[i+1]--;
            a.num[i] += 10;
        }
        ans.num[ans.len++] = a.num[i] - b.num[i];
    }
    while(ans.len - 1 >= 1 && ans.num[ans.len-1] == 0) ans.len--;
    return ans;
}
  • 除法:
Bign Divid(Bign a, int b, int & r)
{
    Bign ans;
    ans.len = a.len;    //对齐
    for(int i = a.len - 1; i >= 0; --i)
    {
        r = r * 10 + b;
        if(r < b)   //不够除商0
        {
            ans.num[i] = 0;
        }else   //够除商余数除dig
        {
            ans.num[i] = r / b;
            r = r % b;  //得到新的余数
        }
    }
    while(ans.len - 1 >= 1 && ans.num[ans.len - 1] == 0) ans.len--;
    return ans;
}

二、用string存储

减法不好做:因为借位可能会使0 -> -1,char没法表示-1;

  • 对齐:
void Align(string & a, string & b)
{
    while(a.size() < b.size()) a.insert(0, "0");
    while(b.size() < a.size()) b.insert(0, "0");
}
  • 加法:
string Add(string a, string b)
{
    Align(a, b);
    string ans;
    int len1 = a.size(), len2 = b.size(), carry = 0;
    for(int i = 0; i < len1 || i < len2; ++i)
    {
        int da = a[len1-1-i] - '0', db = b[len2-1-i] - '0';
        int tmp = da + db + carry;
//        ans.insert(0, to_string(tmp % 10 + '0'));
        ans += (tmp % 10 + '0');
        carry = tmp / 10;
    }
    if(carry)
    {
//        ans.insert(0, to_string(carry + '0'));
        ans += (carry + '0');
    }
    reverse(ans.begin(), ans.end());
    return ans;
}
  • 乘法:
string Prod(string a, int b)
{
    string ans;
    int len1 = a.size(), carry = 0;
    for(int i = 0; i < len1; ++i)
    {
        int da = a[i] - '0';
        int tmp = da * b + carry;
        ans += (tmp % 10 + '0');
        carry = tmp / 10;
    }
    while(carry)
    {
        ans += (carry % 10 + '0');
        carry /= 10;
    }
    reverse(ans.begin(), ans.end());
    return ans;
}
  • 除法:
string Divid(string a, int b, int & r)
{
    string ans = a;
    for(int i = 0; i < a.size(); ++i)
    {
        r = r * 10 + (a[i] - '0');
        if(r < b)
        {
            ans[i] = '0';   //注意这里是字符'0',而不是数字0
        }else
        {
            ans[i] = (r / b + '0');
            r = r % b;
        }
    }
    while(ans.size() > 1 && ans[0] == '0') ans.erase(0, 1);
    return ans;
}

2. 数据结构篇

2.1 线性表:

TIPS 1 :注意没有有效节点,即空链表,要特殊处理如 PAT 1052

处理思路 1:排序+筛选

  • 思路:

Plan 1. 在原链表上直接排序,要额外用标记flg来筛选出有效节点,同时因为排序后下标不在是初始地址,故应额外用add记录源地址。

struct Node{
	int add, data, next;
	bool flg;
}node[maxn];
bool cmp(Node a, Node b){
	if(a.flg != b.flg) return a.flg > b.flg;	//true的放前面
	else .... //题目规定排序规则
}

输出:

if(cnt == 0)	//没有有效节点
else{
	for(int i = 0; i < cnt; ++i){
		if(i < cnt-1) printf("%05d %d %05d", node[i].add, node[i].data, node[i+1].add);
		else printf("%05d %d -1", node[i].add, node[i].data);
	}
}

处理思路 2:下标(地址)排序

Plan 2. 将地址(下标),存入一个vector数组,然后下标排序,排序后格式化输出(推广出一种链表题的通法:按题目规则将链表每个节点地址存入vector中,再格式化输出vector)

struct Node{
	int data, nex;
}node[maxn];
bool cmp(int add1, int add2){
	node[add1].data ? node[add2].data...	//排序规则
}

输出:

void Print(vector<int> & ans){
	if(ans.size() == 0)	//没有有效节点:单独处理
	else{
		for(int i = 0; i < ans.size(); ++i){
			if(i < ans.size()-1)	printf("%05d %d %05d", ans[i], node[ans[i]].data, ans[i+1]);
			else printf("%05d %d -1", ans[i], node[ans[i]].data);
		}
	}
}

2.2 树:

树的遍历:

DFS(前中后:递归实现 )【注】 有时间学习下非递归实现方法

  • 用vector 记录路径的模板
vector<int> tmp, ans;
void DFS(int id, ...)
{
	tmp.push_back(id);
	if(遍历到叶节点)
	{	
		//求解,优化各标尺
		tmp.pop_back();		//因为下面return回溯到上一层,所以要"手动"将这层的节点pop出去
		return;
	}
	for()
	{
		DFS(nex, ...);
	}
	tmp.pop_back();	//遍历完id的所有后继了,id没用了 将pop其出去
}

BFS(层序:队列实现)

TIPS:
STL中的queue,push操作只是push进去一个副本
对原变量的修改不改变队列中的副本
若要修改队列中元素,应push进去元素的编号(如数组下标、变量的地址,即指针),而不是元素本身

  • 静态树版:
void BFS(int r)
{
    queue<int> q;
    q.push(r);
    while(!q.empty())
    {
        int now = q.front();
        /*访问当前节点*/
        q.pop();
        /*子节点入队*/
        if(!q.empty()) printf(" "); //可以控制输出空格
    }
}
  • 普通树版:
void BFS(node* r)
{
    queue<node*> q;
    q.push(r);
    while(!q.empty())
    {
        node* now = q.front();
        /*访问当前节点*/
        q.pop();
        /*子节点入队*/
        if(!q.empty()) printf(" "); //可以控制输出空格
    }
}

【注】 静态树和普通版本差别不大,后面都只留普通版本了

小技巧

  1. 找根:如果根未显示给出

Plan A: 三叉树:设置父节点

输入完以后不断向上遍历,直到父节点为空

Plan B: 可以用has表记录有父亲的节点,在遍历一遍has表,找到没有父亲的节点,即为根

Plan C: 如果题目规定节点编号连续,可以利用数学知识求出根的编号:

因为节点连续,所以所有节点编号的和 r = n * (n - 1) / 2 (节点从0开始编号) 或 (n + 1) * n / 2 (节点从1开始编号)

上面的节点编号和,再输入过程中不断减去作为孩子的节点的编号,最后剩下的就是根

【对与空节点处理】由定理:二叉树的空指针个数 = 节点数 + 1 (可由 n0 = n2 + 1 得出:将所有空指针补上节点(肯定是叶节点即n0后,原来所有的节点都变成了n2)),所以最开始的时候编号和 减去 空指针,这样输入时遇到空指针可以直接当做-1看待

以编号从0开始为例:

int r = (n * (n - 1) / 2) - (n + 1);
for(int i = 0; i < n; ++i)
{
    r += (lc + rc);	//空指针 令lc / rc 为 -1
}
  1. 涉及CBT(完全二叉树) 或 到求层序遍历序列:可以考虑用数组存储法

1)将数组初始化为-1(用于标记空指针)

2)根节点位于node[1]

3)任一节点node[id],左孩子为node[2 * id], 右孩子为 node[2 * id + 1]

可用先序/中序/后序遍历,将树填入数组,然后顺序遍历非空节点

int node[maxn];
memset(node, -1, sizeof(node));
void Pre(int id, int root)
{
    if(root == -1) return;
    node[id] = root;
    Pre(2 * id, node[root].lc);
    Pre(2 * id + 1, node[root].rc);
}
  • 打印结果
printf("%d", node[1]);
for(int i = 2; i < maxn; ++i)
{
	if(node[i] != -1) printf(" %d", node[i]);
}

例1: PAT A1102
例2: PAT A1110

【补充】如果节点个数很多,担心爆数组,可以用结构体 + priority_queue 或 set来"压缩"数组

struct node
{
    int data, id;
    bool operator < (const node & tmp) const { return id > tmp.id; }
}
priority_queue<node> seq;
set<node> seq;
  1. 判断CBT的方法:

Plan A: 数组法,2中已叙述

Plan B:层序遍历,如果遍历到空节点说明已经正常的话已经“到了头”,如果后面还能遍历到非空节点,这棵树就不是CBT,由此可以的到思路:层序遍历 遍历到空指针后“上锁”, 后面如果再次进入非空节点,isCBT = false;

while(!q.empty())
{
    int now = q.front();
    q.pop();
    if(node[now].lc != -1)
    {
        if(lock == true) isCBT = false;
        q.push(node[now].lc);
    }else lock = true;
    if(node[now].rc != -1)
    {
        if(lock == true) isCBT = false;
        q.push(node[now].rc);
    }else lock = true;
}

建树:

TIPS 1
中序+(先序,后序,层序):唯一
先序+后序:不唯一

TIPS 2
若选择闭区间(如 create(0, n-1) ):[left, right] 退出条件为 left > right
若选择开区间(如 create(0, n) ):[left, right) 退出条件为 left >= right

处理思路 1: 先建树再对树遍历
  • 普通树版
void Create(node* & r, int postL, int postR, int inL, int inR)
{
    if(postL >= postR) return;
    int now = post[postR-1], id = inL;
    r = NewNode(now);
    while(id < inR && in[id] != now) id++;
    int numL = id - inL;
    Create(r->lc, postL, postL + numL, inL, id);
    Create(r->rc, postL + numL, postR - 1, id + 1, inR);
}
  • 改进:在中序序列中找根的过程,可以提前用一个map记录每个节点在中序中的位置
unordered_map<int, int> pos_in;	//存放节点在in中的位置:【注】不能用vector 可能会段错误(越界)
void Create()
{
	...
    int now = post[postR-1], id = pos_in[now], numL = id - inL;
	... 
}
处理思路 2: 直接递归遍历

Create()相当于 PreOrder()…,在相应的地方插入访问语句即可如:

void Create()
{
	...
	printf("%d", r->data);
	Create(); //左子树建树
	Create(); //右子树建树
}

二叉查找树 BST

重要性质: BST的中序序列是有序的!比如给出前序序列隐含给出了中序序列(排序即可),就可以前中建树

插入:
void Insert(node* & r, int x)
{
    if(r == NULL)
    {
        r = NewNode(x);
        return;
    }
    if(x < r->data)
    {
        Insert(r->lc, x);
    }else
    {
        Insert(r->rc, x);
    }
}
删除

规则:<老二当大哥> 一个山寨老大被干掉了(删除),怎么办?把二哥找来!

  • 找二哥:前驱 / 后继
GetPre(r->lc);
node* GetPre(node* root)
{
	while(node != NULL)
	{
		root = root->rc;
	}
	return root;
}
GetNex(r->rc);
node* GetNex(node* root)
{
	while(root != NULL)
	{
		root = root->lc;
	}
	return root;
}
  • 删除:

普通版: 有左孩子递归删前驱,没有递归删后继,都没有直接删(找到了)

void Delete(node* root, int x)
{
    if(root == NULL) return;
    if(x < root->data)
    {
        Delete(root->lc, x);
    }else if(x > root->data)
    {
        Delete(root->rc, x);
    }else 
    {
        if(root->lc != NULL)
        {
            node* pre = GetPre(root->lc);
            root->data = pre->data;
            Delete(root->lc, pre->data);
        }else if(root->rc != NULL)
        {
            node* nex = GetNex(root->rc);
            root->data = nex->data;
            Delete(root->rc, nex->data);
        }else
        {
            root = NULL;
        }
    }
}
  • 优化版: 找到前驱 / 后继 后 直接将子树移栽
node* Delete(node* r, int x)
{
    if(!r) return NULL;
    if(x < r->data)
    {
        r->lc = Delete(r->lc, x);
    }else if(x > r->data)
    {
        r->rc = Delete(r->rc, x);
    }else
    {
        if(r->lc != NULL)
        {
            node* pre = GetPre(r->lc);
            r->data = pre->data;
            r->lc = Delete(r->lc, pre->data);
        }else if(r->rc != NULL)
        {
            node* nex = GetNex(r->rc);
            r->data= nex->data;
            r->rc = Delete(r->rc, nex->data);
        }else
        {
            r = NULL;
        }
    }
    return r;
}
  • 写法2:
void Delete(node* &root, int x){
	if(root == NULL) return;
	if(root->data == x){
		if(root->lchild == NULL && root->rchild == NULL) root = NULL;
		else if(root->lchild != NULL){
		//左子树不空
			node * pre = findMax(root->lchild);	//!!!TIPS 1:pre一定没有右孩子 
			if(pre->lchild == NULL) pre = NULL;
			else{
				pre->father->lchild = pre->lchild;
				pre = NULL;
			} 
		}else{
			node * next = findMin(root->rchild);
			if(next->rchild == NULL) next = NULL;
			else{
				next->father->rchild = next->rchild;
				next = NULL;
			}
		}
	}else if(root->data > x){
		Delete(root->lchild, x);
	}else{
		Delete(root->rchild, x);
	}
}

练习:leetcode 450: Delete Node in a BST

AVL

插入:
  • 要点:
  1. 结构体中加一个height变量;
  2. NewNode():height = 1;
  3. GetHeight(): r == NULL时return 0; (空节点高0)
  4. UpdateHeight(): 要用3的GetHeight()获得子树高,而不是直接r->lc.height
  5. UpdateBalanceFactor(): 同4 要用GetHeight()计算
  6. L(); R(); 要使用&, 注意更新高度的顺序,先root,后tmp,最后将root = tmp;
  7. Insert(): 要使用&, 每次递归插入后更新该节点(root),在看root是否失衡
  • 结构
struct node
{
    int data, height;
    node *lc, *rc;
};
  • NewNode()
node* NewNode(int x)
{
    node* r = new node;
    r->data = x;
    r->height = 1;
    r->lc = r->rc = NULL;
    return r;
}
  • GetHeight()
int GetHeight(node* r) { return r == NULL ? 0 : r->height; }
  • UpdateHeight()
void UpdateHeight(node* r) { r->height = max(GetHeight(r->lc), GetHeight(r->rc)) + 1; }
  • GetBalanceFactor()
int GetBalanceFactor(node* r) { return GetHeight(r->lc) - GetHeight(r->rc); }
  • L / R
void L(node* & r)
{
    node* tmp = r->rc;
    r->rc = tmp->lc;
    tmp->lc = r;
    UpdateHeight(r);
    UpdateHeight(tmp);
    r = tmp;
}
void R(node* & r)
{
    node* tmp = r->lc;
    r->lc = tmp->rc;
    tmp->rc = r;
    UpdateHeight(r);
    UpdateHeight(tmp);
    r = tmp;
}
  • Insert()
void Insert(node* & r, int x)
{
    if(r == NULL)
    {
        r = NewNode(x);
        return;
    }
    if(x < r->data)
    {
        Insert(r->lc, x);
        UpdateHeight(r);
        if(GetBalanceFactor(r) == 2)
        {
            if(GetBalanceFactor(r->lc) == -1) L(r->lc);
            R(r);
        }
    }else
    {
        Insert(r->rc, x);
        UpdateHeight(r);
        if(GetBalanceFactor(r) == -2)
        {
            if(GetBalanceFactor(r->rc) == 1) R(r->rc);
            L(r);
        }
    }
}
删除

略…

Heap

核心:
向下调整 DownAdjust(); -> 建堆MakeHeap(), 删除 Delete(), 堆排序 HeapSort()
向上调整 UpAdjust(); -> 插入 Insert()

【注】堆排序的过程就是不断交换堆尾和堆顶,并删除堆顶的过程

  • DownAdjust()
void DownAdjust(vector<int> & heap, int low, int high)
{
    int now = low, lc = 2 * low;
    while(lc <= high)
    {
        if(lc + 1 <= high && heap[lc+1] > heap[lc]) lc = lc + 1;
        if(heap[lc] > heap[now])
        {
            swap(heap[now], heap[lc]);
            now = lc;
            lc = 2 * now;
        }else break;
    }
}
  • MakeHeap()
void MakeHeap(vector<int> & heap, int n)
{
    for(int i = n / 2; i > 0; --i)
    {
        DownAdjust(heap, i, n);
    }
}
  • Delete()
void Delete(vector<int> & heap)
{
    heap[1] = heap.back();
    heap.pop_back();
    DownAdjust(heap, 1, heap.size()-1);
}
  • 示例:
    源序列: 3 1 2 8 7 5 9 4 6 0
    建堆后: 9 8 5 6 7 3 2 4 1 0
    删除后: 8 7 5 6 0 3 2 4 1

  • HeapSort()

不断输出heapTop(最大值),再deleteTop()

void HeapSort(vector<int> & heap, int n)
{
    MakeHeap(heap, n);
    for(int i = n; i > 1; --i)
    {
        swap(heap[1], heap[i]);
        DownAdjust(heap, 1, i - 1);
    }
}
  • UpAdjust()

将插入元素放到最后一位,将其不断和父亲PK,直到比父亲小或者到了根

void UpAdjust(vector<int> & heap, int low, int high)
{
    int now = high, father = now / 2;
    while(father >= low)
    {
        if(heap[father] < heap[now])
        {
            swap(heap[father], heap[now]);
            now = father;
            father = now / 2;
        }else break;
    }
}
  • Insert()
void Insert(vector<int> & heap, int x)
{
    heap.push_back(x);
    UpAdjust(heap, 1, heap.size()-1);
}
  • 补充:isHeap()
void isHeap(vector<int> const & heap, int high)
{
    bool is_max = true, is_min = true;
    for(int i = high; i > 1; --i)
    {
        int father = i / 2;
        if(heap[i] > heap[father]) is_max = false;
        if(heap[i] < heap[father]) is_min = false;
    }
    if(is_max == is_min)
    {
        printf("Not Heap\n");
    }if(is_max)
    {
        printf("Max Heap\n");
    }else if(is_min)
    {
        printf("Min Heap\n");
    }
}
  • Algorithm中的heap函数
函数 函数原型 作用
is_heap() bool is_heap( RandomIt first, RandomIt last, Compare comp); 判断区间是否为heap
is_heap_until() RandomIt is_heap_until( RandomIt first, RandomIt last, Compare comp ); 找出区间中第一个不满足heap条件的位置.
make_heap() make_heap( RandomIt first, RandomIt last, Compare comp); 在指定区间建堆
push_haep() void push_heap( RandomIt first, RandomIt last, Compare comp ); 把指定区间的最后一个元素插入到heap中
pop_heap() void pop_heap( RandomIt first, RandomIt last, Compare comp ); 弹出heap顶元素, 将其放置于区间末尾.
sort_heap() void sort_heap( RandomIt first, RandomIt last, Compare comp ); 堆排序,make_heap后反复调用pop_heap来实现

【注】cmp:less()是默认的大顶堆(Max Heap), greater<>() 小顶堆

哈夫曼树

#include 
#include 
using namespace std;
priority_queue<long long, vector<long long>, greater<long long> > q;

int main(){
	int n;
	long long tmp, x, y, ans = 0;
	scanf("%d", &n);
	for(int i = 0; i < n; ++i){
		scanf("%lld", &tmp);
		q.push(tmp);
	}
	while(q.size() > 1){
		x = q.top();
		q.pop();
		y = q.top();
		q.pop();
		q.push(x+y);
		ans += x + y; 
	}
	printf("%lld", ans);
	return 0;
} 

LCA

Node *find_lca(Node *node, int x, int y) {
    if (node == nullptr || node->val == x || node->val == y)
        return node;
    Node *left = find_lca(node->left, x, y);
    Node *right = find_lca(node->right, x, y);
    if (left != nullptr && right != nullptr)
        return node;
    return left == nullptr ? right : left;
}

并查集(Disjoint-set)

对于实现并查集要关心的问题:

  1. 用什么来唯一标识一个集合(应用:查找一个元素所在的集合,或查找两元素是否属于同一集合)
  2. 如何得到集合的信息(比如集合的大小,集合成员的各种属性和…)
  3. 对集合的操作(主要是合并操作)
版本1:father[i] = i;
  • 对3个问题的实现是:
  1. 每个集合的根节点总是指向自己(father[i] = i;),其他节点指向父亲节点,不断向上遍历终到根节点

  2. 信息少的比如只获得集合大小, 最简单的是输入结束后遍历每个节点(cnt[FindFather(i)]++ ) 来统计每个集合的节点个数; 也可以额外设置数组cnt[i]对应集合i的人数; 信息多时, 可以使用结构体来记录信息,其中设置f 变量起到 father[i]的作用, 其余变量根据题目需要设置与初始化
    【注】查询方法有两种 :
    1)可以输入结束后, 遍历每个节点来统计各集合的信息;
    2)在每次Union操作时都将信息累加到新的根节点上,最后集合的整体信息就在根节点上了

  3. 合并操作:先判断两个节点是否属于一个集合1, 若不属于同一集合,将其中一个指向另一个即可

这里实现一个带计数功能的并查集,输入节点编号为 1 ~ n

  • 初始化:
void Init(int n){

    for(int i = 0; i <= n; ++i)
    {
        cnt[i] = 1;
        father[i] = i;
    }
}
  • 查找:带路径压缩
int FindRoot(int x)
{
    int tmp = x;
    while(father[x] != x)
    {
        x = father[x];
    }
    while(father[tmp] != tmp)
    {
        int tmp2 = tmp;
        tmp = father[tmp];
        father[tmp2] = x;
    }
    return x;
}
  • 合并:带合并计数
void Union(int a, int b)
{
    int ra = FindRoot(a), rb = FindRoot(b);
    if(ra != rb)
    {
        father[ra] = rb;    //将ra指向rb
        cnt[rb] += cnt[ra]; //集合人数累加到rb(cnt[rb] = 当前总人数)
    }
}
版本2:father[i] = -1;
  • 对三个问题的实现:
  1. 首先用什么标记集合(根)呢?这个版本中,每个集合中只有father[根] < 0(其绝对值代表集合大小),其他节点和版本1一样都是指向父节点,这样一个集合就可以被唯一标记,这个根的显著特点是他的father[根]为负数,同时也可以通过abs(father[根])来获得集合大小;
    对于初始情况,father[i] = -1; 代表 集合i 只有1个元素;
  2. 查询大小直接可以:abs(father[FindRoot(x)]);
  3. 合并操作:先将集合的大小累加到总根上去(这里选rb作为总根,实际可选小集合作为总根 防止链化)father[rb] += father[ra]; , 再将一个集合的根指向总根father[ra] = rb;
  • 初始化:
memset(father, -1, sizeof(father));
  • 查找:
int FindRoot(int x)
{
    int tmp = x;
    while(father[x] > 0)
    {
        x = father[x];
    }
    while(father[tmp] > 0)
    {
        int tmp2 = tmp;
        tmp = father[tmp];
        father[tmp2] = x;
    }
    return x;
}
  • 合并:
void Union(int a, int b)
{
    int ra = FindRoot(a), rb = FindRoot(b);
    if(ra != rb)
    {
        if(father[ra] < father[rb]) //fb更小
        {
            father[rb] += father[ra];
            father[ra] = rb;
        }else
        {
            father[ra] += father[rb];
            father[rb] = ra;
        }
    }
}

2.3 图

顶点处理

  • 对于只有一种“性质”的节点:输入是int型,直接作为顶点编号

若输入为string类型,将其映射为int类型,其中idex记录输入总人数

int idex = 0;
unordered_map<string, int> get_id;
unordered_map<int, string> get_name;
int Change(string v)
{
    if(get_id.find(v) == get_id.end())
    {
        get_id[v] = idex;
        get_name[idex] = v;
        return idex++;
    }else return get_id[v];
}
  • 对于有几种性质的顶点:可以将其映射到不同的数段(如加油站0~300, 居民区301~310 ),
    这种情况要【注意】路径是否每种节点都可经过,以及可以经过的次数;

图的遍历

  • DFS:见树的遍历

补充 1:对于一些无向图实现单向遍历的问题,可以通过“注释”掉回头的路径来解决, 如 PAT 1034 ,或对某些存在环的图,想要遍历到一个联通分量的所有边(用vis注释掉访问过的点,只能保证遍历完所有点,环的最后一条边遍历不到)

void DFS(int id, ... )
{
    vis[id] = true;
    //更新点标尺
    for(int i = 0; i < idex; ++i)
    {
        if(G[id][i] != 0)
        {
            //更新边标尺
            G[i][id] = 0;
            if(vis[i] == false)
            {
                DFS(i, ... );
            }
        }
    }
}
void DFSTraversal()
{
    for(int i = 0; i < idex; ++i)
    {
        if(vis[i] == false)
        {
            DFS(i, ... );
        }
    }
}

补充 2:
对于一些最优化问题,可能路径存在环,遍历过的节点(中转站)不能简单的“注释”掉,因为其他路径上可能还会经过这些点。
采用的策略是, 从这个点出发时将这个点注释掉,防止它的遍历后继时回头,遍历完所有后继以后再取消该点的注释
换句话说,保护当前路径不成环,且不,回头(类似栈: 当前路径上每个访问过的节点都置为true压入栈,当这条路径访问结束后,没回溯一次,解放一个顶点)

void DFS(int id, ... )
{
    if(...) return;
    vis[id] = true; //保证不回头,且当前路径不成环
    for(int i = 0; i < G[id].size(); ++i)
    {
        int nex = G[id][i];
        if(vis[nex] == false)
        {
            DFS(nex, ... );
        }
    }
    vis[id] = false;    
}
  • BFS:注意图的BFS相比树的BFS多了一个bool vis[maxn] 用来标记入过队的元素,这是因为树是自顶向下有向传递的; 而图没有一个唯一起点,我们必须人为的设置一个起点(中心),并以此向外“单向”扩散, 具体操作为:每次只入队未入队过的元素,并在元素入队后将其标记为已入队(true)
void BFS(int start)
{
    //多次查询,每次都要初始化状态数组
    memset(vis, false ,sizeof(vis));
    L[start] = 0;
    vis[start] = true;
    queue<int> q;
    q.push(start);
    while(!q.empty())
    {
        int now = q.front();
        q.pop();
        //对出队元素的操作
        for(int i = 0; i < G[now].size(); ++i)
        {
            int nex = G[now][i];
            if(vis[nex] == false)
            {
                vis[nex] = true;
                L[nex] = L[now] + 1;
                q.push(nex);
            }
        }
    }
}

最短路径

单源最短路径:从起点start到其他各点的最短路径
要点:每轮得到一个最优结果now,并以此为中介点来优化其他d[]
多个标尺时:优化第一个标尺的同时,也要更新第二个标尺(优化第一标尺说明已经选择了该点push进路径, 后面遇到两难处境时, 是在这个存档点的基础上比较第二标尺)

  • 练习路线:纯DFS(回溯剪枝) -> (纯Dijkstra -> Dijkstra+DFS) -> 堆优化版Dijkstra - > 堆优化版Dijkstra+DFS -> BF -> BF + DFS -> SPFA -> SPFA + DFS

争取每道题都从头到尾用所有方法AC一遍(另外注意不同存储方式的写法与适用场合,PAT的题大部分邻接矩阵就可以(节点数 1000):邻接矩阵,邻接表), 邻接矩阵方便记录边的权值,如果顶点数过多邻接表会更快一些,也可以两种搭配使用,如 北京地铁 的DFS写法2;

Dijkstra
  • 普通版:二重循环

外层控制优化轮数V, 内层找最优解V+优化其他点(邻接矩阵V, 邻接表E) ,复杂度:邻接矩阵 O(V2), 邻接表 O(V2 + E), 总的来说都是O(V2)的复杂度

其中:外层要将每个节点置为已访问,至少V次无法优化,内层找dmin的过程可通过堆来优化

const int maxn = 1010, INF = 0x3fffffff;
int G[maxn][maxn], d[maxn], weight[maxn], dw[maxn], cnt[maxn];
bool vis[maxn];

void Dijkstra(int start, int destination, int n)
{
    fill(d, d + maxn, INF);
//    memset(vis, false, sizeof(vis));  //只使用一次就不用初始化了
    d[start] = 0; dw[start] = weight[start]; cnt[start] = 1;
    for(int i = 0; i < n; ++i)
    {
        int mid = -1, Min = INF;
        for(int j = 0; j < n; ++j)
        {
            if(vis[j] == false && d[j] < Min)
            {
                Min = d[j];
                mid = j;
            }
        }
        if(mid == -1 && mid == destination) return;
        vis[mid] = true;
        for(int j = 0; j < n; ++j)
        {
            if(vis[j] == false && G[mid][j] != 0)
            {
                if(d[mid] + G[mid][j] < d[j])
                {
                    d[j] = d[mid] + G[mid][j];
                    dw[j] = dw[mid] + weight[j];
                    cnt[j] = cnt[mid];
                }else if(d[mid] + G[mid][j] == d[j])
                {
                    if(dw[mid] + weight[j] > dw[j])
                    {
                        dw[j] = dw[mid] + weight[j];
                    }
                    cnt[j] += cnt[mid];
                }
            }
        }
    }
}

缺点:边权非负

  • 堆优化版:使用priority_queue 来优化内层中找dmin的过程

时间复杂度为:O(VlogV + E)

const int maxn = 1010, INF = 0x3fffffff;
int G[maxn][maxn], d[maxn], weight[maxn], dw[maxn], cnt[maxn];
bool vis[maxn];
struct node
{
    int id, d;
    bool operator < (const node & tmp) const { return d > tmp.d; }
};

void Dijkstra(int start, int destination, int n)
{
    fill(d, d + maxn, INF);
//    memset(vis, false, sizeof(vis));    //只使用一次就不用初始化了
    d[start] = 0; dw[start] = weight[start]; cnt[start] = 1;
    priority_queue<node> q;
    q.push(node{start, 0});
    while(!q.empty())
    {
        node now = q.top();
        q.pop();    //1.这个千万别忘了!
        if(vis[now.id]) continue;   //2. 这个千万别忘了!
        if(now.id == destination) return;
        vis[now.id] = true;
        for(int i = 0; i < n; ++i)
        {
            if(vis[i] == false && G[now.id][i] != 0)
            {
                if(now.d + G[now.id][i] < d[i])
                {
                    d[i] = now.d + G[now.id][i];
                    dw[i] = dw[now.id] + weight[i];  //3. 这个前往别忘了!
                    cnt[i] = cnt[now.id];
                    q.push(node{i, d[i]});  //4. 这个前往别忘了!
                }else if(now.d + G[now.id][i] == d[i])
                {
                    if(dw[now.id] + weight[i] > dw[i])
                    {
                        dw[i] = dw[now.id] + weight[i];
                        q.push(node{i, d[i]});
                    }
                    cnt[i] += cnt[now.id];  //注意:这里统计数量必须放在外面,因为即使第二标尺不优化,遇到同长路径也要更新最短路数
                }
            }
        }
    }
}
  • 多个标尺:Dijkstra + DFS

Dijkstra中增加一个vector pre[maxn] 路径树,记录从destination出发到start的所有最短路径

vector<int> pre[maxn];
void Dijkstra(int start, int destination, int n)
{
    fill(d, d + maxn, INF);
    d[start] = 0;
    priority_queue<node> q;
    q.push(node{start, 0});
    while(!q.empty())
    {
        node now = q.top();
        q.pop(); 
        if(vis[now.id]) continue;  
        if(now.id == destination) return;
        vis[now.id] = true;
        for(int i = 0; i < n; ++i)
        {
            if(vis[i] == false && G[now.id][i] != 0)
            {
                if(now.d + G[now.id][i] < d[i])
                {
                    d[i] = now.d + G[now.id][i];
                    pre[i].clear();
                    pre[i].push_back(now.id);
                    q.push(node{i, d[i]});  
                }else if(now.d + G[now.id][i] == d[i])
                {
                    pre[i].push_back(now.id);
                    q.push(node{i, d[i]});
                }
            }
        }
    }
}

DFS中用tmp记录当前路径,用ans记录最终路径,其余变量按题目要求来设计即可

vector<int> tmp, ans;   //如果需要保留路径 -> ans
void DFS(int id, int start, int & cnt)
{
    tmp.push_back(id);
    if(id == start)
    {
        cnt++;	//求最短路径数
        for(int i = tmp.size()-1; i >= 0; --i)
        {
        	遍历路径得到这条路径的各项“属性”
        }
        //判断这条路径是否是最优路径,是则更新
        tmp.pop_back();
        return;
    }
    for(int i = 0; i < pre[id].size(); ++i)
    {
        int nex = pre[id][i];
        DFS(nex, start, cnt, max_weight);
    }
    tmp.pop_back();
}
BF

解决单源最短路径,可解决有负权边的问题

思路:
类似一种递推的过程,若一个图存在最短路,则它的最短路径树唯一确定。
设置源点(即最短路径树的根)root的d[root] = 0;
每轮松弛所有边,这样保证最短路径树的第2层的d全部被优化;下一轮第3层d全部被优化,直到最底层被优化结束。Q1
因为节点的个数为V, 最短路径树的层数最大为V,循环轮数最多为V-1,且当某一轮中没有一个节点被优化,说明已经到了最底层,可以直接退出。
但是若图中存在负环,优化V-1轮后再次对所有边松弛,依然有顶点可以被优化(说明存在负环return false;)

【Q1】为什么每轮都要松弛所有边呢,不能只松弛对应层的边吗?
答:因为最短路径树是结果(由果不能推因),上面的讨论都是基于最短路径树,但最短路径树在构建成之前并不是确定的。无法推测那个节点属于那一层,但松弛所有节点必定会将属于那一层的节点的d递推出来。 由此我们有了优化的思路:以层为标尺来优化,进而得到SPFA算法(类似于BFS)

Ps. 以上全是我瞎猜,尚未考证…

对于前驱数组的求法:因为每个节点可能被多次访问,故应用set来保存前驱数组,若求最短路径数,要么DFS,要么每次遇到==,重现统计最短路径数(累加所有前驱的最短路径树)

时间复杂度:邻接表O(VE) , 邻接矩阵O(V3)

  • 两个标尺直接BF
const int maxn = 1010, INF = 0x3fffffff;
int d[maxn], weight[maxn], dw[maxn], cnt[maxn];
struct node
{
    int nex, dis;
};
vector<node> G[maxn];
set<int> pre[maxn];
bool BF(int start, int n)
{
    fill(d, d + maxn, INF);
    d[start] = 0; dw[start] = weight[start]; cnt[start] = 1;
    for(int i = 0; i < n-1; ++i)
    {
	    bool isUpdate = false;
        for(int u = 0; u < n; ++u)
        {
            for(int j = 0; j < G[u].size(); ++j)
            {
                int v = G[u][j].nex;
                int len = G[u][j].dis;
                if(d[u] + len < d[v])
                {
                    d[v] = d[u] + len;
                    dw[v] = dw[u] + weight[v];
                    cnt[v] = cnt[u];
                    pre[v].clear();
                    pre[v].insert(u);
                    isUpdate = true;
                }else if(d[u] + len == d[v])
                {
                    if(dw[u] + weight[v] > dw[v])
                    {
                        dw[v] = dw[u] + weight[v];
                        isUpdate = true;
                    }
                    pre[v].insert(u);
                    cnt[v] = 0; //别忘了清零!!
                    for(auto it : pre[v]) cnt[v] += cnt[it];
                }
            }
        }
        if(!isUpdate) return true;
    }
    for(int u = 0; u < n; ++u)
    {
        for(int j = 0; j < G[u].size(); ++j)
        {
            int v = G[u][j].nex;
            int len = G[u][j].dis;
            if(d[u] + len < d[v])
            {
                return false;
            }
        }
    }
    return true;
}
  • 两个标尺BF+DFS

BF:

const int maxn = 1010, INF = 0x3fffffff;
int d[maxn];
struct node
{
    int nex, dis;
};
vector<node> G[maxn];
set<int> pre[maxn];
bool BF(int start, int n)
{
    fill(d, d + maxn, INF);
    d[start] = 0;
    for(int i = 0; i < n-1; ++i)
    {
        for(int u = 0; u < n; ++u)
        {
            for(int j = 0; j < G[u].size(); ++j)
            {
                int v = G[u][j].nex;
                int len = G[u][j].dis;
                if(d[u] + len < d[v])
                {
                    d[v] = d[u] + len;
                    pre[v].clear();
                    pre[v].insert(u);
                }else if(d[u] + len == d[v])
                {
                    pre[v].insert(u);
                }
            }
        }
    }
    for(int u = 0; u < n; ++u)
    {
        for(int j = 0; j < G[u].size(); ++j)
        {
            int v = G[u][j].nex;
            int len = G[u][j].dis;
            if(d[u] + len < d[v])
            {
                return false;
            }
        }
    }
    return true;
}

DFS:

vector<int> tmp, ans;
void DFS(int id, int start)
{
    tmp.push_back(id);
    if(id == start)
    {
        cnt++;
        int sum_hands = 0;
        for(int i = tmp.size()-1; i >= 0; --i)
        {
            //计算当前路径的各标尺
        }
        //优化各标尺, 并更新最优解
        tmp.pop_back();
        return;
    }
    for(auto it : pre[id])
    {
        DFS(it, start);
    }
    tmp.pop_back();
}
SPFA

SPFA是对BF的优化:BF中有大量无意义的操作,比如 只有一个顶点的d被改变时, 它邻接点的d才可能跟着改变。 故可以用一个队列来收纳刚被更新过的节点,每次队首出队更新他的所有邻接边,直到队空! 但是!!!注意这里有个细节:如果被更新过的元素正在队中,就不将其push进队了,为什么呢?因为如果将其push进队后,队中前后有两个相同的节点,这两个节点是等效的,前一个节点只能通过他的d1+邻接边长来更新邻接点,后一个节点也是通过d2+邻接边长来更新其邻接点,d1和d2一定时相等的,从最短路径树的角度,更新一定是自顶向下,单向传递的(这也是SPFA和BFS相像的原因吧);从图的角度,若d1经过一系列操作后变小了,它下一次还可以进行同样的一系列操作变小,无限变小,说明存在负环。

对于负环:如果一个节点入队次数超过V-1,说明存在负环

【注】 以上分析没有理论依据,是我瞎猜的…

  • SPFA:
int weight[maxn], d[maxn], dw[maxn], cnt[maxn];
struct node
{
    int nex, dis;
};
vector<node> G[maxn];
int cnt_inq[maxn];
bool inq[maxn];
unordered_set<int> pre[maxn];
bool SPFA(int start, int n)
{
    fill(d, d + maxn, INF);
    d[start] = 0; dw[start] = weight[start]; cnt[start] = 1;
    queue<int> q;
    q.push(start);
    inq[start] = true;
    cnt_inq[start]++;
    while(!q.empty())
    {
        int now = q.front();
        q.pop();
        inq[now] = false;
        for(int i = 0; i < G[now].size(); ++i)
        {
            int nex = G[now][i].nex;
            int dis = G[now][i].dis;
            if(d[now] + dis < d[nex])
            {
                d[nex] = d[now] + dis;
                dw[nex] = dw[now] + weight[nex];
                cnt[nex] = cnt[now];
                pre[nex].clear();
                pre[nex].insert(now);
                if(inq[nex] == false)
                {
                    q.push(nex);
                    inq[nex] = true;
                    cnt_inq[nex]++;
                    if(cnt_inq[nex] >= n) return false;
                }
            }else if(d[now] + dis == d[nex])
            {
                if(dw[now] + weight[nex] > dw[nex])
                {
                    dw[nex] = dw[now] + weight[nex];
                }
                if(inq[nex] == false)   //nex是一条新的最短路分支,要将nex加入队列,所有应写在上面那个if的外面!!
                {
                    q.push(nex);
                    inq[nex] = true;
                    cnt_inq[nex]++;
                    if(cnt_inq[nex] >= n) return false;
                }
                pre[nex].insert(now);
                cnt[nex] = 0;
                for(int it : pre[nex]) cnt[nex] += cnt[it];
            }
        }
    }
    return true;
}
  • SPFA + DFS

SPFA:

const int maxn = 1010, INF = 0x3fffffff;
int d[maxn], numq[maxn];
bool inq[maxn];
struct node
{
    int nex, dis;
};
vector<node> G[maxn];
set<int> pre[maxn];
bool SPFA(int start, int n)
{
    fill(d, d + maxn, INF);
    queue<int> q;
    q.push(start);
    inq[start] = true;  //在队中
    numq[start]++;  //入队次数
    d[start] = 0;
    while(!q.empty())
    {
        int mid = q.front();
        q.pop();
        inq[mid] = false;
        for(int i = 0; i < G[mid].size(); ++i)
        {
            int nex = G[mid][i].nex;
            int len = G[mid][i].dis;
            if(d[mid] + len < d[nex])
            {
                d[nex] = d[mid] + len;
                pre[nex].clear();
                pre[nex].insert(mid);
                if(inq[nex] == false)
                {
                    q.push(nex);
                    inq[nex] = true;
                    numq[nex]++;
                    if(numq[nex] >= n) return false;
                }
            }else if(d[mid] + len == d[nex])
            {
           		if(inq[nex] == false)
                {
                    q.push(nex);
                    inq[nex] = true;
                    numq[nex]++;
                    if(numq[nex] >= n) return false;
                }
                pre[nex].insert(mid);
            }
        }
    }
    return true;
}

DFS: 同BF的DFS

Floyd

解决全源最短路径问题, 邻接矩阵O(V3), 200 个节点以内

要点: d[i][j] 表示从i 到 j 的最短距离

Floyd思路:枚举所有(i , j ) 在其中插入k (每个顶点),看是否能优化

int d[maxn][maxn];
void Floyd(int n)
{
    for(int k = 0; k < n; ++k)
    {
        for(int i = 0; i < n; ++i)
        {
            for(int j = 0; j < n; ++j)
            {
                if(d[i][k] != INF && d[k][j] != INF)
                {
                    if(d[i][k] + d[k][j] < d[i][j])
                    {
                        d[i][j] = d[i][k] + d[k][j];
                    }
                }
            }
        }
    }
}

最小生成树

prim

只是将Dijkstra中更新操作改为:if(G[mid][nex] < d[nex]) 更新, 即dijkstra中d[i] 的含义是 i 到源点的最短距离,而prim中的含义是到当前最小生成树节点集合的最短距离
时间复杂度O(V2)

同样可以通过堆优化 后 O(VlogV + E)

  • 普通版:
const int maxn = 110, INF = 0x3fffffff;
int d[maxn], G[maxn][maxn];
bool vis[maxn];
int Prim(int start, int n)
{
    fill(d, d + maxn, INF);
    memset(vis, false, sizeof(vis));
    d[start] = 0;
    int ans = 0;
    for(int i = 0; i < n; ++i)
    {
        int mid = -1, Min = INF;
        for(int j = 0; j < n; ++j)
        {
            if(vis[j] == false && d[j] < Min)
            {
                Min = d[j];
                mid = j;
            }
        }
        if(mid == -1) return -1;
        vis[mid] = true;
        ans += d[mid];
        for(int j = 0; j < n; ++j)
        {
            if(vis[j] == false && G[mid][j] < d[j])
            {
                d[j] = G[mid][j];
            }
        }
    }
    return ans;
}
  • 堆优化版:
const int maxn = 110, INF = 0x3fffffff;
int d[maxn], G[maxn][maxn];
bool vis[maxn];
struct node
{
    int id, d;
    bool operator < (const node & tmp) const { return d > tmp.d; }
};
int Prim(int start, int n)
{
    fill(d, d + maxn, INF);
    memset(vis, false, sizeof(vis));
    d[start] = 0;
    priority_queue<node> q;
    q.push(node{start, 0});
    int ans = 0;
    while(!q.empty())
    {
        node now = q.top();
        q.pop();
        if(vis[now.id]) continue;
        vis[now.id] = true;
        ans += d[now.id];
        for(int i = 0; i < n; ++i)
        {
            if(vis[i] == false && G[now.id][i] != INF)
            {
                if(G[now.id][i] < d[i])
                {
                    d[i] = G[now.id][i];
                    q.push(node{i, d[i]});
                }
            }
        }
    }
    return ans;
}
kruskal

思路: 每次选最小的边,若Judge()成功,加入最小生成树

先对边排序,然后依次遍历所有的边,符合条件的加入,当:边数==顶点数-1 时成功,所有边遍历完以后若不满足边数 == 顶点数-1 ,则失败

那么,Judge() 是什么呢?这个边要满足加入后不成环:即他的两个顶点不在一个联通块内(集合内),否则就形成了环, 这可以通过并查集来实现

时间复杂度 O(ElogE)

  • 普通版
const int maxn = 110;
struct edge
{
    int v1, v2, len;
};
int father[maxn];
void Init(int n) { for(int i = 0; i <= n; ++i) father[i] = i;}
int FindRoot(int x)
{
    int tmp = x;
    while(father[x] != x)
    {
        x = father[x];
    }
    while(father[tmp] != tmp)
    {
        int tmp2 = tmp;
        tmp = father[tmp];
        father[tmp2] = x;
    }
    return x;
}
bool cmp(edge a, edge b) { return a.len < b.len; }
int Kruskal(int nv, int ne, vector<edge> & ans)
{
    Init(nv);
    int sum = 0, numE = 0;
    sort(ans.begin(), ans.end(), cmp);
    for(int i = 0; i < ne; ++i)
    {
        int ra = FindRoot(ans[i].v1), rb = FindRoot(ans[i].v2);
        if(ra != rb)
        {
            father[ra] = rb;
            sum += ans[i].len;
            numE++;
            if(numE == nv-1) break;
        }
    }
    if(numE != nv-1) return -1;
    else return sum;
}
  • 堆优化版:
const int maxn = 110;
struct edge
{
    int v1, v2, len;
    bool operator < (const edge & tmp) const { return len > tmp.len; }
};
int father[maxn];
void Init(int n) { for(int i = 0; i <= maxn; ++i) father[i] = i;}
int FindRoot(int x)
{
    int tmp = x;
    while(father[x] != x)
    {
        x = father[x];
    }
    while(father[tmp] != tmp)
    {
        int tmp2 = tmp;
        tmp = father[tmp];
        father[tmp2] = x;
    }
    return x;
}
int Kruskal(int nv, int ne, priority_queue<edge> & pq)  //输入时将所有边push进pq
{
    Init(nv);
    int sum = 0, numE = 0;
    while(!pq.empty())
    {
        edge now = pq.top();
        pq.pop();
        int ra = FindRoot(now.v1), rb = FindRoot(now.v2);
        if(ra != rb)
        {
            numE++;
            sum += now.len;
            father[ra] = rb;
            if(numE == nv-1) break;
        }
    }
    if(numE != nv-1) return -1;
    else return sum;
}

拓扑排序

用途:判断有向无环图(DAG:Directed Acyclic Graph)

核心:
step 1:初始: 将入度为0节点入队
step 2: 队首出队,遍历其邻接点,将所有邻接点入度- -, 减到0的入队

=>如果结束时,如果队的节点数==总节点数:拓扑排序成功,G为DAG;否则说明图中有环

  • 实现:邻接矩阵+栈
const int maxn = 110;
int G[maxn][maxn], inD[maxn];
bool vis[maxn];
vector<int> TopologicalSort(int n)
{
    stack<int> q;
    vector<int> ans;
    for(int i = 0; i < n; ++i)
    {
        if(inD[i] == 0)
        {
            q.push(i);
        }
    }
    while(!q.empty())
    {
        int now = q.top();
        q.pop();
        ans.push_back(now);        
        vis[now] = true;
        for(int i = 0; i < n; ++i)
        {
            if(vis[i] == false && G[now][i] != INF)
            {
                inD[i]--;
                if(inD[i] == 0)
                {
                    q.push(i);
                }
            }
        }
    }
    if(ans.size() != n) return ERROR;
    return ans;
}
  • 邻接表+队列(如果要求先输出小编号,可用priority_queue 或 set)
const int maxn = 1010;
vector<int> G[maxn];
int inD[maxn];
bool TopologicalSort(int n)
{
    priority_queue<int, vector<int>, greater<int> > pq;
    for(int i = 1; i <= n; ++i)
    {
        if(inD[i] == 0)
        {
            pq.push(i);
        }
    }
    int num = 0;
    while(!pq.empty())
    {
        int now = pq.top();
        pq.pop();
        num++;
        printf("%d", now);
        for(int i = 0; i < G[now].size(); ++i)
        {
            int nex = G[now][i];
            inD[nex]--;
            if(inD[nex] == 0)
            {
                pq.push(nex);
            }
        }
        if(!pq.empty()) printf(" ");
    }
    printf("\n");
    if(num != n) return false;
    else return true;
}

关键路径

AOV网:Activity On Vertex
        ↓ 升级
AOE网:Activity On Edge

关键路径:AOE网中的最长路径(也是工程完成的最短时间,这里的最短是指至少,因为如果要拖延起来,时间不是无限长…唉)

若只求最长路径长度:BF和SPFA都可以求带负权的最短路径长度,故将所有权值取反求最短结果就是最长的相反数

求解DAG的最长简单路径(Longest Path Problem)可用关键路径:

  • 要点:最重要的就是始终记住:事件 对应 只表示状态活动 对应 具有长度
  1. 关键路径找的是不能拖延的活动,而活动对应的是边edge(事件对应点vertex)
  2. 什么样的活动不能拖延呢:最早发生时间和最迟发生时间相等 e[i] == l[i] (开始了就得做,莫得一点解释的时间)
  3. e[i] 和 l[i]怎么求呢? e[i] = 事件u(前驱)的最早发生时间ve[u], l[i] = 事件v(后继)的最迟发生时间-活动持续时间(边长),即vl[v] - len[i]
    【注】e[i]和l[i]只是理解上的东西,实际可以用ve[]和vl[]求出来,不需要实际开数组
  4. 必须所有前期工作做完才能开始,所以最早ve[u] = max(前期工作事件ve[前驱] + len[前驱->u]) : 正向拓扑排序; 相应的,使后序工作都能不超时(vl[后继])的最早时间,就是vl[v] = min(后序工作事件-len[边] vl[后继] - len[v->后继]), 逆拓扑排序
    【注1】同一张图,正拓扑:每次出队入度为0的点;而逆拓扑:每次出队出度为0的点,互为逆过程;所以逆拓扑序列 = reverse(正拓扑序列),这一过程可以通过stack来实现
    【注2】 由于都是有向图,正拓扑求ve[]时 可以以当前节点向后更新,逆拓扑求vl[]时 可以用后面节点更新自己
初始将 ve数组 置为 0
正拓扑求ve, 同时节点入栈
vl数组 置为 maxLen
出栈, 逆拓扑求vl, 同时夹出关键活动

实现:

const int maxn = 110;
int inD[maxn];
stack<int> reTopo;
struct node
{
    int nex, len;
};
vector<node> G[maxn];
int ve[maxn], vl[maxn];
  1. CriticalPath() 返回关键路径长度(最长路径)
int CriticalPath(int n, int & start)
{
    memset(ve, 0, sizeof(ve));
    if(TopologicalSort(n) == false) return -1;
    int max_len = 0;
    for(auto it : ve) max_len = max(max_len, it);
    fill(vl, vl + maxn, max_len);
    while(!reTopo.empty())
    {
        int now = reTopo.top();
        reTopo.pop();
        if(reTopo.empty()) start = now;
        for(int i = 0; i < G[now].size(); ++i)
        {
            int nex = G[now][i].nex;
            int len = G[now][i].len;
            if(vl[nex] - len < vl[now]) //如果当前能被更新(可以更小)
            {
                vl[now] = vl[nex] - len;
            }
        }
    }
    return max_len;
}
  1. TopologicalSort()
bool TopologicalSort(int n) //顶点0 ~ n-1
{
    queue<int> q;
    for(int i = 0; i < n; ++i)
    {
        if(inD[i] == 0)
        {
            q.push(i);
        }
    }
    while(!q.empty())
    {
        int now = q.front();
        q.pop();
        reTopo.push(now);
        for(int i = 0; i < G[now].size(); ++i)
        {
            int nex = G[now][i].nex;
            int len = G[now][i].len;
            if(ve[now] + len > ve[nex]) //如果下一个能被更新(可以更大)
            {
                ve[nex] = ve[now] + len;
            }
            if(--inD[nex] == 0)
            {
                q.push(nex);
            }
        }
    }
    if(reTopo.size() != n)) return false;
    else return true;
}
  1. DFS() 求出关键路径
void DFS(int now)
{
    for(int i = 0; i < G[id].size(); ++i)
    {
        int nex = G[id][i].nex;
        int len = G[id][i].len;
        if(ve[now] + len == vl[nex])
        {
            printf("(%d->%d) ", now, nex);
            DFS(nex);
        }
    }
}
  • 之前照算法笔记实现的一个版本:
/*求AOE网的关键路径*/
/*先求点(事件),再夹边(活动)*/
/*
公式:
a.活动(a)的最早开始时间{用数组e[a]记录}为u的最早开始时间{ve[u]}
b.最晚开始时间{用数组l[a]记录}为u的最晚开始时间{vl[u]},即v的最晚开始时间{vl[v]}-a花费的时间{cost[a]}
求e[a]和l[a]=>转化为求ve[u]和ve[v]
事件u开始的前提是:为工程中开始u的所有条件(所有前驱)都完成,最慢的一条线路完成了,u才能开始,可以抽象为:ve[u] = max{ve[preu]+cost[preu->u]} 
同理v最迟也要保证:v所有后继都能在最迟时间(deadline)前开始,即vl[v] = min{vl[postv]-cost[v->postv]}
用算法表示为:
拓扑排序求ve[],逆拓扑排序求vl[]:从第一个节点开始,不断"优化"他的所有后继(相对正向或逆向来说),等进行到他的后继时,他的后继已经被所有前驱优化过了,一定是最优的结果,即所求的ve或vl(这里是一种"递推"的思路,用条件(已知)不断优化结果(未知),如果所有条件都优化完,结果一定最优了;因为条件找结果容易,结果反过来找条件就不容易了)
判断是关键活动的条件是:e[a] == l[a],即u->v之间的活动,没有一点弹性空间,到手就得开始做->关键活动! 
*/ 
/*邻接表实现*/
#include 
#include 
#include 
#include  
using namespace std;
const int maxn = 510;
const int INF = 0x3fffffff;
int n, m;
vector<int> G[maxn];
vector<int> revG[maxn];	//保存逆向图 
int weight[maxn][maxn];	//存放所有边的权值 
int inDegree[maxn];	//存放每个顶点的入度 
//vector topo;	//存储拓扑序列=》不需要,在拓扑排序的同时就求出了ve[] 
stack<int> revtopo;	//存储逆拓扑序列 
int ve[maxn], vl[maxn];	//存放每个顶点(事件)的最早最迟开始时间 


bool TopoLogicalSort(){
	queue<int> q;
	int cntInq = 0;
	for(int i = 0; i < n; ++i){
		if(inDegree[i] == 0){
			q.push(i);
		}
	}
	while(!q.empty()){
		int now = q.front();
		revtopo.push(now);	//改为入栈,方便逆序输出 逆拓扑序列 
		q.pop();
		for(int i = 0; i < G[now].size(); ++i){
			inDegree[G[now][i]]--;
			if(inDegree[G[now][i]] == 0){
				q.push(G[now][i]);
			} 
		//正向:求ve[] 优化i的所有后继
			int next = G[now][i];
			if(ve[now] + weight[now][next] > ve[next]){
				ve[next] = ve[now] + weight[now][next];
			}
		}
//		G[now].clear();		//不能有这句,否则后面逆拓扑的vl无法求了 
//		cntInq++;	//可以省略,就等于revtopo.size() 
	}
	if(revtopo.size() == n)	return true;
	else return false;      
} 
int CriticalPath(){
	fill(ve, ve+maxn, 0);
	if(TopoLogicalSort() == false){
		return -1;	//顺便执行了 TopoLogicalSort()
	}
	//如果事先不知道汇点,取ve[]的最大值(最晚发生的事件),即汇点 
	int maxve = 0;
	for(int i = 0; i < n; ++i){
		if(ve[i] > maxve)
			maxve = ve[i];
	}
	fill(vl, vl+maxn, maxve);	//要用最后一个节点(汇点)的vl值(==ve值)初始化vl 
	while(!revtopo.empty()){
	//逆向:求vl[] 
		int now = revtopo.top();
		revtopo.pop();
		for(int i = 0; i < revG[now].size(); ++i){
		//优化i的所有后继
			int next = revG[now][i];
			if(vl[now] - weight[now][next] < vl[next]){
				vl[next] = vl[now] - weight[now][next];
			} 
		} 
	}
	for(int i = 0; i < n; ++i){
		for(int j = 0; j < G[i].size(); ++j){
			int next = G[i][j];
			if(vl[next] - weight[i][next] == ve[i])
				printf("%d->%d ", i, next);
		}
	}
	return maxve;	
}
void Output(){
	printf("\n-----------\n");
	for(int i = 0; i < n; ++i)
		printf("%d ", i);
	printf("\n-----------\n");
	for(int i = 0; i < n; ++i){
		printf("%d ", ve[i]);
	}
	printf("\n");
	for(int i = 0; i < n; ++i){
		printf("%d ", vl[i]);
	}
	printf("\n-----------\n");
	printf("关键节点为:\n");
	for(int i = 0; i < n; ++i){
		if(vl[i] - ve[i] == 0)
			printf("%d ", i);
	}
}
void test1(){
//测试1:图的输入 : 
	for(int i = 0; i < n; ++i){
		printf("%d: ", i);
		for(int j = 0; j < G[i].size(); ++j){
			printf("%d->%d(w=%d)", i, G[i][j], weight[i][G[i][j]]);
		}
		printf("\n");
	}
}
void test2(){
//测试2:逆序拓扑序列 
//!!!:不能用stack.size()作为for的终止条件,因为每轮for都会pop,使size减小!
	int sizes = revtopo.size(); 
	for(int i = 0; i < sizes; ++i){
		printf("%d ", revtopo.top());
		revtopo.pop();
	}
}

int main(){
	scanf("%d %d", &n, &m);
	int u, v, w;
	fill(inDegree, inDegree+n, 0);
	fill(weight[0], weight[0]+maxn*maxn, INF);
	for(int i = 0; i < m; ++i){
		scanf("%d %d %d", &u, &v, &w);
		weight[u][v] = w;	
		weight[v][u] = w;
		G[u].push_back(v);
		revG[v].push_back(u);
		inDegree[v]++;//v每被加入到邻接表一次,v的入度+1 
	}
	CriticalPath();
	Output();
//	test1();
//	test2();
}
/*
//5个顶点,7条边 无环 
5 7
0 1 3 
0 3 2 
0 4 8 
1 3 4 
1 2 1 
2 4 5 
3 4 7 
Sample Output
0->1 1->3 3->4
-----------
0 1 2 3 4
-----------
0 3 4 7 14
0 3 9 7 14
-----------
关键节点为:
0 1 3 4
*/ 

你可能感兴趣的:(PAT,A)