11.29先补充到LCA,改天再说Orz
已补完

图的存储

直接存边

struct Edge
{
    int u, v;
} int n, m;

vector<Edge> e;
vector<bool> vis;

bool find_edge(int u, int v)
{
    for (int i = 1; i <= m; i++)
    {
        if (e[i].u == u && e[i].v)
        {
            return true;
        }
    }
    return false;
}

void dfs(int u)
{
    if (vis[u])
        return;
    vis[u] = true;
    for (int i = 1; i <= m; i++)
    {
        if (e[i].u == u)
        {
            dfs(e[i].v);
        }
    }
}

int main()
{
    cin >> n >> m;
    vis.resize(n + 1, false);
    e.resize(m + 1);
    for (int i = 1; i <= m; i++)
        cin >> e[i].u >> e[i].v;
    return 0;
}

邻接矩阵


int n, m;

vector<vector<bool>> adj;
vector<bool> vis;

bool find_edge(int u, int v)
{
    return adj[u][v];
}

void dfs(int u)
{
    if (vis[u])
        return;
    vis[u] = true;
    for (int v = 1; v <= n; v++)
    {
        if (adj[u][v])
            dfs(v);
    }
}

int main()
{
    cin >> n >> m;
    vis.resize(n + 1, false);
    adj.resize(n + 1, vector<bool>(n + 1, false));
    for (int i = 1; i <= m; i++)
    {
        int u, v;
        cin >> u >> v;
        adj[u][v] = true;
    }
    return 0;
}

邻接表


int n, m;
vector<vector<bool>> adj;
vector<bool> vis;

bool find_edge(int u, int v)
{
    for (int i = 0; i < adj[u].size(); i++)
    {
        if (adj[u][i] == v)
        {
            return true;
        }
    }
    return false;
}

void dfs(int u)
{
    if (vis[u])
        return;
    vis[u] = true;
    for (int i = 0; i < adj[u].size(); i++)
    {
        dfs(adj[u][i]);
    }
}

int main()
{
    cin >> n >> m;
    vis.resize(n + 1, false);
    adj.resize(n + 1);
    for (int i = 1; i <= m; i++)
    {
        int u, v;
        cin >> u >> v;
        adj[u].push_back(v);
    }
return 0;
}

链式前向星

//head[u]和cnt初始值都为-1
void add(int u, int v)
{
    nxt[++cnt] = head[u];
    head[u] = cnt;
    to[cnt] = v;
}

//遍历u的出边
for (int i = head[u]; ~i; i = nxt[i])
{
    int v = to[i];
}

DFS

void dfs(int u)
{
    vis[u] = 1;
    for (int i = head[u]; i; i = e[i].x)
    {
        if (!vis[e[i].t])
        {
            dfs(v);
        }
    }
}

BFS

void bfs(int u)
{
    while (!Q.empty())
        Q.pop;
    Q.push(u);
    vis[u] = 1;
    d[u] = 0;
    p[u] = -1;
    while (!Q.empty())
    {
        u = Q.front();
        Q.pop();
        for (int i = head[u]; i; i = e[i].x)
        {
            if (!vis[e[i].t])
            {
                Q.push(e[i].t);
                vis[e[i].t] = 1;
                d[e[i].t] = d[u] + 1;
                p[e[i].t] = u;
            }
        }
    }
}
void restore(int x)
{
    vector<int> res;
    for (int v = x; v = -1; v = p[v])
    {
        res.push_back(v);
    }
std:
    reverse(res.begin(), res.end());
    for (int i = 0; i < res.size(); i++)
        printf("%d", res[i]);
    puts("");
}

树的直径

两次 DFS

第一次求任意一点的最远点,第二次求这个最远点的最远点,后两次节点连线即为直径

int n, c, d[N];
vector<int> E[N];
void dfs(int u, int fa)
{
    for (int v : E[u])
    {
        if (v == fa)
            continue;
        d[v] = d[u] + 1;
        if (d[v] > d[c])
            c = v;
        dfs(v, u);
    }
}

int main()
{
    cin >> n;
    for (int i = 1; i <= n; i++)
    {
        int u, v;
        cin >> u >> v;
        E[u].push_back(v);
        E[v].push_back(u);
    }
    dfs(1, 0);
    d[c] = 0;
    dfs(c, 0);
    cout << d[c] << '\n';
    return 0;
}

树形 DP

以1为树的根,每个节点作为子树的根向下,所能延伸的最大距离和次大距离之和

int n, d = 0;
int d1[N], d2[N];
vector<int> E[N];
void dfs(int u, int fa)
{
    d1[u] = d2[u] = 0;
    for (int v : E[u])
    {
        if (v == fa)
            continue;
        dfs(v, u);
        int t = d1[v] + 1;
        if (t > d1[u])
            d2[u] = d1[u], d1[u] = t;
        else if (t > d2[u])
            d2[u] = t;
    }
    d = max(d, d1[u] + d2[u]);
}
int main()
{
    cin >> n;
    for (int i = 1; i < n; i++)
    {
        int u, v;
        cin >> u >> v;
        E[u].push_back(v);
        E[v].push_back(u);
    }
    dfs(1, 0);
    cout << d << '\n';
    return 0;
}

LCA

倍增

预处理fa(x,i)(表示x的第2^i个祖先)(用dfs)

将y次跳转优化为"y的二进制表示所含的1的个数"次跳转

vector<int> v[MXN];
vector<int> w[MXN];

int fa[MXN][31], cost[MXN][31], dep[MXN];
int n, m;
int a, b, c;

//lca初始化,接收起始节点和父亲节点
void dfs(int root, int fno)
{
    //初始化,2^0=1个祖先就是父亲节点,dep比父亲节点+1
    fa[root][0] = fno;
    dep[root] = dep[fa[root][0]] + 1;
    //初始化:其他的祖先节点:第2^i的祖先节点是第x^(n-1)个祖先节点的第2^(n-1)个祖先节点
    for (int i = 1; i < 31; i++)
    {
        fa[root][i] = fa[fa[root][i - 1]][i - 1];
        cost[root][i] = cost[fa[root][i - 1]][i - 1] + cost[root][i - 1];
    }
    //遍历子节点进行dfs
    int sz = v[root].size();
    for (int i = 0; i < sz; i++)
    {
        if (v[root][i] == fno)
            continue;
        cost[v[root][i]][0] = w[root][i];
        dfs(v[root][i], root);
    }
}

//lca,用倍增算法算取x和y的lca节点。
int lca(int x, int y)
{
    //令x比y深。
    if (dep[x] > dep[y])
        swap(x, y);
    //令y和x在一个深度。
    int tmp = dep[y] - dep[x], ans = 0;
    for (int j = 0; tmp; ++j, tmp >>= 1)
        if (tmp & 1)
            ans += cost[y][j], y = fa[y][j]
                               //如果这个时候y=x,那么x,y就都是他们自己的祖先。
                               if (y == x) return ans;
    //不然的话,找到第一个不是它们祖先的两个点。
    for (int j = 30; j >= 0 && y != x; --j)
    {
        if (fa[x][j] != fa[y][j])
        {
            ans += cost[x][j] + cost[y][j];
            x = fa[x][j];
            y = fa[y][j];
        }
    }
    //返回结果。
    ans += cost[x][0] + cost[y][0];
    return ans;
}

int main()
{
    //初始化表示祖先的数组fa,代价cost和深度dep。
    memset(fa, 0, sizeof(fa));
    memset(cost, 0, sizeof(cost));
    memset(dep, 0, sizeof(dep));
    //读入树:节点数总共n个。
    cin >> n;
    for (int i = 1; i <= n; i++)
    {
        cin >> a >> b >> c;
        a++, b++;
        v[a].push_back(b);
        v[b].push_back(a);
        w[a].push_back(c);
        w[b].push_back(c);
    }
    //为了计算lca而使用dfs。
    dfs(1, 0);
    cin >> m;
    for (int i = 0; i < m; i++)
    {
        cin >> a >> b;
        a++, b++;
        cout << lca(a, b) << endl;
    }
    return 0;
}

Tarjan

使用 并查集 记录某个结点的祖先结点。做法如下:

1.首先接受输入(邻接链表)、查询(存储在另一个邻接链表内)。查询边其实是虚拟加上去的边,为了方便,每次输入查询边的时候,将这个边及其反向边都加入到 queryEdge 数组里。
2.然后对其进行一次 DFS 遍历,同时使用 visited 数组进行记录某个结点是否被访问过、parent 记录当前结点的父亲结点。
3.其中涉及到了 回溯思想,我们每次遍历到某个结点的时候,认为这个结点的根结点就是它本身。让以这个结点为根节点的 DFS 全部遍历完毕了以后,再将 这个结点的根节点 设置为 这个结点的父一级结点。
4.回溯的时候,如果以该节点为起点,queryEdge 查询边的另一个结点也恰好访问过了,则直接更新查询边的 LCA 结果。
5.输出结果。

class Edge
{
public:
    int toVertex, fromVertex;
    int next;
    int LCA;
    Edge() : toVertex(-1), fromVertex(-1), next(-1), LCA(-1){};
    Edge(int u, int v, int n) : fromVertex(u), toVertex(v), next(n), LCA(-1){};
};

int head[MAX], queryHead[MAX];
Edge edge[MAX], queryEdge[MAX];
int parent[MAX], visited[MAX];
int vertexCount, edgeCount, queryCount;

void init()
{
    for (int i = 0; i <= vertexCount; i++)
    {
        parent[i] = i;
    }
}

int find()
{
    if (parent[x] == x)
    {
        return x;
    }
    else
    {
        return find(parent[x]);
    }
}
void tarjan(int u)
{
    parent[u] = u;
    visited[u] = 1;

    for (int i = head[u]; i != -1; i = edge[i].next)
        ;
    {
        Edge &e = edge[i];
        if (!visited[e.toVertex])
        {
            tarjan(e.toVertex);
            parent[e.toVertex] = u;
        }
    }

    for (int i = queryHead[u]; i != -1; i = queryEdge[i].next)
    {
        Edge &e = queryEdge[i];
        if (visited[e.toVertex])
        {
            queryEdge[i ^ 1].LCA = e.LCA = find(e.toVertex);
        }
    }
}
int main()
{
    memset(head, 0xff, sizeof(head));
    memset(queryHead, 0xff, sizeof(queryHead));
    cin >> vertexCount >> edgeCount >> queryCount;
    int count = 0;
    for (int i = 1; i < edgeCount; i++)
    {
        int start = 0, end = 0;
        cin >> start >> end;

        edge[count] = Edge(start, end, head[start]);
        head[start] = count;
        count++;
    }
    count = 0;
    for (int i = 0; i < queryCount; i++)
    {
        int start = 0, end = 0;
        cin >> start >> end;
        queryEdge[count] = Edge(start, end, head[start]);
        queryHead[start] = count;
        count++;
        queryEdge[count] = Edge(end, start, head[end]);
        queryHead[end] = count;
        count++;
    }
    init();
    tarjan(i);
    for (int i = 0; i < queryCount; i++)
    {
        Edge &e = queryEdge[i * 2];
        cout << e.LCA << endl;
    }
    return 0;
}

树的重心

定义:对于树上的每一个点,计算其所有子树中最大的子树节点数,这个值最小的点就是这棵树的重心

性质:
所有的子树大小都不超过整棵树大小的一半
树中所有点到某个点的距离和中,到重心的距离和是最小的;如果有两个重心,那么到它们的距离和一样
把两棵树通过一条边相连得到一棵新的树,那么新的树的重心在连接原来两棵树的重心的路径上
在一棵树上添加或删除一个叶子,那么它的重心最多只移动一条边的距离

求法:
DFS计算每棵子树的大小,记录“向下”的子树的最大大小,利用总点数-当前子树(这里的子树指有根树的子树)的大小得到“向上”的子树的大小,然后就可以依据定义找到重心了。

//这份代码默认节点编号从1开始
int size[MAX],    //这个节点的大小(所有子树上的节点数+本节点
    weight[MAXN], //这个节点的重量
    centroid[2];  //用于记录树的重心(存的是节点编号
void GetCentroid(int cur, int fa)
{ //cur表示当前节点
    size[cur] = 1;
    weight[cur] = 0;
    for (int i = head[cur]; i != -1; i = e[i].nxt)
    {
        if (e[i].to != fa)
        { //e[i].to表示这个点通向的节点
            GetCentroid(e[i].to, cur);
            size[cur] += size[e[i].to];
            weight[cur] = max(weight[cur], size[e[i].to]);
        }
    }
    weight[cur] = max(weight[cur], n - size[cur]);
    if (weight[cur] <= n / 2)
    { //按照树的重心的定义统计
        centroid[centroid[0] != 0] = cur;
    }
}

拓扑排序

Kahn 算法

初始:集合S装所有入度0的点,L为空
每次S中取点u放入L,将u的所有边删除,若删除后有点入度为0,则放入S中
重复以上过程,直到S为空。检查图中有无边,若有则图为环路,否则返回L,其顺序即为拓扑排序的结果

fake code

bool toposort()
{
    q = new queue();
    for (int i = 0; i < n; i++)
        if (in_deg[i] == 0)
            q.push(i);
    ans = new vector();
    while (!q.empty())
    {
        u = q.pop();
        ans.push_back(u);
        for each
            edge(u, v)
            {
                if (--in_deg[v] == 0)
                    q.push(v);
            }
    }
    if (ans.size() == n)
    {
        for (int i = 1; i < n; i++)
            cout << ans[i] << endl;
        return true;
    }
    else
    {
        return false;
    }
}


DFS 算法

vector<int> G[MAXN] //邻接表
    int c[MAXN];    //标志数组
vector<int> topo;   //拓扑排序后的节点

bool dfs(int u)
{
    c[u] = -1;
    for (int v : G[u])
    {
        if (c[v] < 0)
            return false;
        else if (!c[v])
            if (!dfs(v))
                return false;
    }
    c[u] = 1;
    topo.push_back(u);
    return true;
}

bool toposort()
{
    topo.clear();
    memset(c, 0, sizeof(c));
    for (int u = 0; u < n; u++)
        if (!c[u])
            if (!dfs(u))
                return false;
    reverse(topo.begin(), topo.end());
    return true;
}

最短路

Floyd 算法

for (int k = 1; k <= n; k++)
{
    for (int x = 1; x <= n; x++)
    {
        for (int y = 1; y <= n; y++)
        {
            f[x][y] = min(f[x][y], f[x][k] + f[k][y]);
        }
    }
}

Bellman-Ford 算法

松弛:dis(v)=min(dis(v),dis(u)+w(u,v))

此算法就是对每一条边进行松弛,直到没有可松弛的边

判负环:若第n次仍存在可松弛的边,则能抵达一个负环

struct edge
{
    int v, w;
};
vector<edge> e[maxn];
int dis[maxn];
bool bellmanford(int n, int s)
{
    memset(dis, 63, sizeof(dis));
    dis[s] = 0;
    bool flag;
    for (int i = 1; i <= n; i++)
    {
        flag = false;
        for (int u = 1; u <= n; u++)
        {
            for (auto ed : e[u])
            {
                int v = ed.v, w = ed.w;
                if (dis[v] > dis[u] + w)
                {
                    dis[v] = dis[u] + w;
                    flag = true;
                }
            }
        }
        //没有可以松弛的边时就停止算法
        if (!flag)
            break;
    }
    //第n轮循环仍然可以松弛时说明s点可以抵达一个负环
    return flag;
}

SPFA 算法(队列优化

上一次被松弛的边,所连接的边才可能引起下一次操作,所以建一个队列维护“哪些节点会引起松弛操作”

判负环:最短路经过至少n条边

struct edge
{
    int v, w;
};
vector<edge> e[maxn];
int dis[maxn], cnt[maxn], vis[maxn];
queue<int> q;
bool spfa(int n, int s)
{
    memset(dis, 63, sizeof(dis));
    dis[s] = 0, vis[s] = 1;
    q.push(s);
    while (!q.empty())
    {
        int u = q.front();
        q.pop(), vis[u] = 0;
        for (auto ed : e[u])
        {
            int v = ed.v, w = ed.w;
            if (dis[v] > dis[u] + w)
            {
                dis[v] = dis[u] + w;
                cnt[v] = cnt[u] + 1; //记录最短路经过的边数
                if (cnt[v] >= n)
                    return false;
                //在不经过负环的情况下,最短路至多经过n-1条边
                //因此如果经过了多于n条边,一定说明经过了负环
                if (!vis[v])
                    q.push(v), vis[v] = 1;
            }
        }
    }
    return true;
}

Warning:无负权边时最好用Dij

Dijkstra 算法

已确定最短路长度的点集为S,未确定的为T。初始化起点为0,其他点无穷大。
从T中选最短路长度最小的点放入S,对加入S的所有出边进行松弛操作,直到T为空。

struct edge
{
    int v, w;
};
struct node
{
    int dis, u;
    bool operator>(const node &a) const
    {
        return dis > a.dis;
    }
};
vector<edge> e[maxn];
int dis[maxn];
bool vis[maxn];
priority_queue<node, vector<node>, greater<node>> q;
void dijkstra(int n, int s)
{
    memset(dis, 63, sizeof(dis));
    dis[s] = 0;
    q.push({0, s});
    while (!q.empty())
    {
        int u = q.top().u;
        q.pop();
        if (vis[u])
            continue;
        vis[u] = 1;
        for (auto ed : e[u])
        {
            int v = ed.v, w = ed.w;
            if (dis[v] > dis[u] + w)
            {
                dis[v] = dis[u] + w;
                q.push({dis[v], v});
            }
        }
    }
}