UMD CP Club Summer CP Week 6
A graph \(G = (V,E)\) is a structure consisting of a vertex set \(V\) and edge set \(E\)
Directed Graph
Undirected Graph
There can be weight on edges, and we call the graph weighted
An outdegree of a vertex \(u\) is the number of edge in a form of \((u,v)\)
An indegree of a vertex \(u\) is the number of edge in a form of \((v,u)\)
An degree of a vertex \(u\) is the number of edges incident to \(u\)
Two vertices \(u,v\) are connected if you can reach \(v\) from \(u\) and \(u\) from \(v\)
The connected components are the set of vertices that are connected
Two vertices \(u,v\) are connected if you can reach \(v\) from \(u\) and \(u\) from \(v\)
The connected components are the set of vertices that are connected
A tree is a connected graph without cycles, it can be rooted or not
A directed acyclic graph (DAG) is a directed graph without cycles
It is possible to find a topological order
A functional graph is a directed graph where all vertices has outdegree exactly 1
A permutation can be drawn into a functional graph
A simple graph is a graph that does not contain self loops or multi-edge
Suppose you are given a graph, and you want to know whether two vertices are adjacent, how will you do it?
Let's start an 2D Array \(A\), and we call it the adjacency matrix
Let's start an 2D Array \(A\), and we call it the adjacency matrix
This enables us to know whether \(u,v\) are adjacent in \(O(1)\), but it requires \(O(|V|^2)\) space
While the previous way makes it easy to know whether two vertices are adjacent
It is actually not too useful for us later on
Is there a way to reduce memory usage?
Consider storing an array of lists \(A\), and \(A[u]\) stores all \(v\) connected to \(u\)
This makes finding whether \(u, v\) are connected into \(O(|V|)\)
But it will make traversing the graph easier
const int N = 5005;
int adj[N][N];
int main(){
int n,m;
cin >> n >> m;
for(int i = 0; i < m; i++){
int u,v;
cin >> u >> v;
adj[u][v] = 1;
adj[v][u] = 1; //if undirected
}
}
Adjacency Matrix
const int N = 1e6+5;
vector<int> adj[N];
int main(){
int n,m;
cin >> n >> m;
for(int i = 0; i < m; i++){
int u,v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u); //if undirected
}
}
Adjacency List
There are two ways of doing graph traversal
DFS (Depth First Search)
BFS (Breath First Search)
We will visit by depth, the idea is similar to using a stack
animation from codeforces
void dfs(int u){
visited[u] = true;
for (int v = 1; v <= n; v++){
if (!adj[u][v] || visited[v]) continue;
dfs(v, u);
}
}
DFS using an adjacency matrix
Time Complexity: \(O(|V|^2)\)
void dfs(int u){
visited[u] = true;
for (int v : adj[u]){
if (visited[v]) continue;
dfs(v, u);
}
}
DFS using an adjacency list
Time Complexity: \(O(|V| + |E|)\)
For breath first search, we search the vertices closest to the starting vertex
We usually use a queue to do this
queue<int> q;
q.push(1);
while(!q.empty()){
int u = q.front(); q.pop();
for(int v = 1; v <= n; v++){
if(!adj[u][v] || visited[v]) continue;
q.push(v);
}
}
BFS with adjacency matrix
Time Complexity: \(O(|V|^2)\)
queue<int> q;
q.push(1);
while(!q.empty()){
int u = q.front(); q.pop();
for(int v : adj[u]){
if(visited[v]) continue;
q.push(v);
}
}
BFS with adjacency list
Time Complexity: \(O(|V| + |E|)\)
A bipartite graph is a graph where you can color the vertices into two colors where no adjacent vertices has same color
Fix a vertex, and try coloring the vertices
If there exists some conflicts, then the graph is not bipartite
Can you draw a graph in one pen stroke without going through same edges
You can prove a conclusion
There is no easy way to find Hamiltonian Path/Circuit
Usually we will have to use Bitmask DP to solve problems related to this
Think about taking courses in university
Some classes have prerequisites, then we can draw it into a graph
Therefore, we have the idea of sorting the vertices by their order
The edges will only go from the vertices in front of the order to back
The algorithm
indeg[u] = 0
indeg[v]
queue<int> q;
vector<int> topo;
for(int i = 1; i <= n; i++){
if(deg[i] == 0)
q.push(i);
}
while(!q.empty()){
int u = q.front(); q.pop();
topo.push_back(u);
for(int v : adj[u]){
deg[v]--;
if(deg[v] == 0)
q.push(v);
}
}
//if topo.size() == n, then the graph is a DAG
We want a data structure that can
We introduce the data structure Disjoint Set Union (DSU)
For an undirected graph, we connect each vertices in the same connected components with a directed arrow
When we want to know whether two vertices are connected
We check if their roots are the same
When we want to know whether two vertices are connected
We check if their roots are the same
We will declare an array dsu
, where dsu[u]
is the vertex \(u\) points to
Initialize the data structure
for(int i = 1; i <= n; i++)
dsu[i] = i;
//or you can do
iota(dsu, dsu+MAXN, 0);
Time Complexity: \(O(|V|)\)
To find the root of each vertex
void find(int u) {
if(dsu[u] == u) return u;
return find(dsu[u]);
}
Time Complexity: \(O(|V|)\)
To connect two vertices, we connect the roots of both vertex
void unite(int u, int v){
u = find(v), v = find(v);
if(dsu[u] == dsu[v]) continue;
dsu[u] = v;
}
Time Complexity: \(O(1)\)
However, notice that finding whether \(u,v\) are connected is same as just doing DFS/BFS from one vertex
How can we speed this up?
The problem for finding is that, every time we will have to go through all vertices on the chain
What if we shorten the chain?
Each time we query a vertex, we connect all the vertices on the path to the root
void find(int u) {
if(dsu[u] == u) return u;
return dsu[u] = find(dsu[u]);
}
Time Complexity: \(O(\log n)\) amortized
There is another way to speed up DSU operations
We maintain the size of each component in sz[rt]
There is another way to speed up DSU operations
We maintain the size of each component in sz[rt]
void unite(int u, int v){
u = find(u), v = find(v);
if(u == v) return;
dsu[u] = v;
sz[v] += sz[u];
}
We connect the smaller to the larger one
Doing this technique, you can prove the complexity will become \(O(\log n)\) amortized
void unite(int u, int v){
u = find(u), v = find(v);
if(u == v) return;
if(sz[u] > sz[v]) swap(u, v);
dsu[u] = v;
sz[v] += sz[u];
}
Time Complexity: \(O(\log n)\) amortized
Both optimizations can be used independently
If you use both optimizations, the time complexity of each operations becomes
\(O(\alpha(n))\) amoritzed
The \(\alpha\) here is the inverse Ackermann function
You can consider it almost as a constant