int gcd(int a, int b)
{
return b == 0 ? a : gcd(b, a % b);
}
gcd(4, 6) = 2;
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和它本身的数(只有1、本身两个正因子)
边界条件: 0 ,1
对一个数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;
}
}
}
从2开始(1
和 0
既不是素数也不是合数),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
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;
}
化简规则: 分母:始终为正,分子为0时取1; 约分除gcd
输出规则:分数分为 整数, 假分数, 真分数
乘法可能会超范围,使用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);
}
void ShowAns(Frac a)
{
a = Reduce(a);
if(a.inf) printf("Inf");
else
{
if(a.up < 0) printf("(");
...
if(a.up < 0) printf(")");
}
}
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;
}
减法不好做:因为借位可能会使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;
}
TIPS 1 :注意没有有效节点,即空链表,要特殊处理如 PAT 1052
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);
}
}
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);
}
}
}
DFS(前中后:递归实现 )【注】 有时间学习下非递归实现方法
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(" "); //可以控制输出空格
}
}
【注】 静态树和普通版本差别不大,后面都只留普通版本了
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)将数组初始化为-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;
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
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);
}
unordered_map<int, int> pos_in; //存放节点在in中的位置:【注】不能用vector 可能会段错误(越界)
void Create()
{
...
int now = post[postR-1], id = pos_in[now], numL = id - inL;
...
}
Create()相当于 PreOrder()…,在相应的地方插入访问语句即可如:
void Create()
{
...
printf("%d", r->data);
Create(); //左子树建树
Create(); //右子树建树
}
重要性质: 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;
}
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
struct node
{
int data, height;
node *lc, *rc;
};
node* NewNode(int x)
{
node* r = new node;
r->data = x;
r->height = 1;
r->lc = r->rc = NULL;
return r;
}
int GetHeight(node* r) { return r == NULL ? 0 : r->height; }
void UpdateHeight(node* r) { r->height = max(GetHeight(r->lc), GetHeight(r->rc)) + 1; }
int GetBalanceFactor(node* r) { return GetHeight(r->lc) - GetHeight(r->rc); }
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;
}
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);
}
}
}
略…
核心:
向下调整 DownAdjust(); -> 建堆MakeHeap(), 删除 Delete(), 堆排序 HeapSort()
向上调整 UpAdjust(); -> 插入 Insert()
【注】堆排序的过程就是不断交换堆尾和堆顶,并删除堆顶的过程
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;
}
}
void MakeHeap(vector<int> & heap, int n)
{
for(int i = n / 2; i > 0; --i)
{
DownAdjust(heap, i, n);
}
}
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);
}
}
将插入元素放到最后一位,将其不断和父亲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;
}
}
void Insert(vector<int> & heap, int x)
{
heap.push_back(x);
UpAdjust(heap, 1, heap.size()-1);
}
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");
}
}
函数 | 函数原型 | 作用 |
---|---|---|
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;
}
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;
}
对于实现并查集要关心的问题:
- 用什么来唯一标识一个集合(应用:查找一个元素所在的集合,或查找两元素是否属于同一集合)
- 如何得到集合的信息(比如集合的大小,集合成员的各种属性和…)
- 对集合的操作(主要是合并操作)
每个集合的根节点总是指向自己(father[i] = i;),其他节点指向父亲节点,不断向上遍历终到根节点
信息少的比如只获得集合大小, 最简单的是输入结束后遍历每个节点(cnt[FindFather(i)]++
) 来统计每个集合的节点个数; 也可以额外设置数组cnt[i]对应集合i的人数; 信息多时, 可以使用结构体来记录信息,其中设置f 变量起到 father[i]的作用, 其余变量根据题目需要设置与初始化
【注】查询方法有两种 :
1)可以输入结束后, 遍历每个节点来统计各集合的信息;
2)在每次Union操作时都将信息累加到新的根节点上,最后集合的整体信息就在根节点上了
合并操作:先判断两个节点是否属于一个集合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] = 当前总人数)
}
}
根
的father[根] < 0
(其绝对值代表集合大小),其他节点和版本1一样都是指向父节点,这样一个集合就可以被根
唯一标记,这个根的显著特点是他的father[根]为负数,同时也可以通过abs(father[根])
来获得集合大小;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;
}
}
}
若输入为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
),补充 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;
}
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进路径, 后面遇到两难处境时, 是在这个存档点的基础上比较第二标尺)
争取每道题都从头到尾用所有方法AC一遍(另外注意不同存储方式的写法与适用场合,PAT的题大部分邻接矩阵就可以(节点数 1000):邻接矩阵,邻接表), 邻接矩阵方便记录边的权值,如果顶点数过多邻接表会更快一些,也可以两种搭配使用,如 北京地铁 的DFS写法2;
外层控制优化轮数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];
}
}
}
}
}
缺点:边权非负
时间复杂度为: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中增加一个vector
路径树,记录从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();
}
解决单源最短路径,可解决有负权边的问题
思路:
类似一种递推的过程,若一个图存在最短路,则它的最短路径树唯一确定。
设置源点(即最短路径树的根)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)
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:
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是对BF的优化:BF中有大量无意义的操作,比如 只有一个顶点的d被改变时, 它邻接点的d才可能跟着改变。 故可以用一个队列来收纳刚被更新过的节点,每次队首出队更新他的所有邻接边,直到队空! 但是!!!注意这里有个细节:如果被更新过的元素正在队中,就不将其push进队了,为什么呢?因为如果将其push进队后,队中前后有两个相同的节点,这两个节点是等效的,前一个节点只能通过他的d1+邻接边长
来更新邻接点,后一个节点也是通过d2+邻接边长
来更新其邻接点,d1和d2一定时相等的,从最短路径树的角度,更新一定是自顶向下,单向传递的(这也是SPFA和BFS相像的原因吧);从图的角度,若d1经过一系列操作后变小了,它下一次还可以进行同样的一系列操作变小,无限变小,说明存在负环。
对于负环:如果一个节点入队次数超过V-1,说明存在负环
【注】 以上分析没有理论依据,是我瞎猜的…
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:
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
解决全源最短路径问题, 邻接矩阵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];
}
}
}
}
}
}
只是将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;
}
思路: 每次选最小的边,若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;
}
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)可用关键路径:
事件
对应 点
只表示状态
, 活动
对应 边
具有长度
e[i] == l[i]
(开始了就得做,莫得一点解释的时间)ve[u]
, l[i] = 事件v(后继)的最迟发生时间-活动持续时间(边长),即vl[v] - len[i]
ve[前驱] + len[前驱->u]
) : 正向拓扑排序; 相应的,使后序工作都能不超时(vl[后继])的最早时间,就是vl[v] = min(后序工作事件-len[边] vl[后继] - len[v->后继]
), 逆拓扑排序逆拓扑序列 = reverse(正拓扑序列)
,这一过程可以通过stack来实现ve[]
时 可以以当前节点向后更新,逆拓扑求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];
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;
}
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;
}
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
*/