UESTC2022暑假前集训 图论

知识点:dijkstra,kruskal,spfa判负环,dinic,tarjan,2-SAT,二分图最大匹配和最大权完美匹配,最小直径生成树,最小树形图,等。

UESTC2022暑假前集训 图论

A-蜘蛛的网 解题报告

题目大意

给出一张无向连通图,求至少断掉几条边能使图不再联通。\(n \le 300, m \le 1000\)

解题思路

我们可以用“最大流最小割定理”解决这个问题。

最大流最小割定理是网络流理论的重要定理。是指在一个网络流中,能够从源点到达汇点的最大流量等于如果从网络中移除就能够导致网络流中断的边的集合的最小容量和。即在任何网络中,最大流的值等于最小割的容量。(来自百度百科)

对于本题的图,我们将每条边的容量设为1,设源点为1,枚举汇点2~n,做n-1次Dinic算法取最小值即可。

《算法竞赛入门经典训练指南》中提到,单次Dinic时间复杂度上界为\(O(n^{2}m)\),如果所有边的容量为1,可以证明复杂度为 \(O(\min(n^{\frac{2}{3}},m^{\frac{1}{2}})m)\)。从而总的“复杂度”不超过\(50\times1000\times300 = 1.5 \times 10^{7}\),可以通过本题。

下面简要介绍一下Dinic的流程

  1. 在残量网络中用BFS构造层次图(把原图中的点按照点到源的距离分“层”,只保留不同层之间的边的图)。
  2. 在层次图中使用DFS进行增广直到不存在增广路。
  3. 重复以上步骤直到无法增广。

Dinic过程中有两种常用的优化:

  1. 当前弧优化

    开一个cur数组记录每一个点当前增广到的边,当其他的DFS增广到的时候,从有效的边开始增广。(已经被增广过的边一定是无效的)

    可以这样实现:

for (int& i = cur[u]; i < G[u].size(); i++)
  1. 剩余量优化

    DFS给当前结点u和“目前为止所有弧的最小残量”a两个参数,当u为汇点或a=0时终止DFS过程。

代码实现

//
// Created by vv123 on 2022/4/9.
//
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 10;
const int INF = 0x3f3f3f3f;
int n, m;

struct Edge {
    int from, to, cap, flow;
};

struct Dinic {
    int s, t;
    bool vis[N];
    int d[N];
    int cur[N];

    vector<Edge> edges;
    vector<int> G[N];
    void AddEdge(int from, int to, int cap) {
        edges.push_back({from, to, cap, 0});
        //edges.push_back({to, from, 0, 0); 如果是有向图.
        edges.push_back({to, from, cap, 0});
        G[from].push_back(edges.size() - 2);
        G[to].push_back(edges.size() - 1);
    }

    bool BFS() {
        memset(vis, 0, sizeof(vis));
        queue<int> Q;
        Q.push(s);
        d[s] = 0;
        vis[s] = 1;
        while (!Q.empty()) {
            int u = Q.front(); Q.pop();
            for (int i = 0; i < G[u].size(); i++) {
                Edge& e = edges[G[u][i]];
                int v = e.to;
                if (!vis[v] && e.cap > e.flow) {
                    vis[v] = 1;
                    d[v] = d[u] + 1;
                    Q.push(v);
                }
            }
        }
        return vis[t];
    }

    int DFS(int u, int a) {
        if (u == t || a == 0) return a;
        int flow = 0, f;
        for (int& i = cur[u]; i < G[u].size(); i++) { //这里取引用,使得u的当前弧被i改变,再次访问到u时,将跳过u已经访问过的支路
            Edge& e = edges[G[u][i]], ee = edges[G[u][i] ^ 1];
            int v = e.to;
            if (d[v] == d[u] + 1 && (f = DFS(v, min(a, e.cap - e.flow))) > 0) {
                e.flow += f;
                ee.flow -= f;
                flow += f;
                a -= f;
                if (a == 0) break;
            }
        }
        return flow;
    }

    int Maxflow(int s, int t) {
        this->s = s; this->t = t;
        int flow = 0;
        while (BFS()) {
            memset(cur, 0, sizeof(cur));
            flow += DFS(s, INF);
        }
        return flow;
    }
};

int x[N], y[N];
int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= m; i++) {
        scanf("%d%d", &x[i], &y[i]);
    }
    int ans = INF;
    int s = 1;
    for (int t = 2; t <= n; t++) {
        Dinic dc;
        for (int i = 1; i <= m; i++)
            dc.AddEdge(x[i], y[i], 1);
        ans = min(ans, dc.Maxflow(s, t));
    }
    printf("%d\n", ans);
    return 0;
}

B-土豆的图 解题报告

题目大意

给有一张带权无向图,第\(i\)个点的颜色为\(c_{i}\)\(d(s,t)\) 表示从点 s 到点 t 的权值最小的路径的权值(定义一条路径的权值为路径上权值最大的边的权值)。

求所有满足\(u < v\), \(|c_u -c_v| \geq L\)的点对\((u,v)\)\(d(u,v)\)之和。

\(n \le 2×10^{5}, m \le 5×10^{5},0 \le c_{i} \le 1×10^{9},0 \le w \le 1×10^{8}\)

解题WA思路

想法如下

建立Kruskal重构树,则每次产生的新点的点权,就是两个子树间的点对的”权值”。

如果没有\(|c_u -c_v| \geq L\) 的限制,则该点的贡献就是两边子树的size相乘再乘上新点点权。

现在可以依次枚举较小子树中的点,求出较大子树中有多少点和它匹配,

把颜色离散化,并二分预处理出每一种颜色不能匹配的最小和最大颜色L和R。树状数组存每种颜色的点的个数,在较大子树的树状数组上查询,则匹配数为size - sum(L,R)。

对每一个新增父节点维护一个树状数组,和一个vector。

每次查询把小子树vector里的点扔到大BIT里查询。
把小子树vector的颜色加到大树状数组上,同时编号push_back到大vector上,新点继承大子树的vector和BIT,可以知道这个过程的复杂度是nlogn的。
把每一个树上结点写成一个结构体,里面有一个指针指向vector,另一个指向BIT。
我的数据结构水平十分有限,没有通过本题(只过了样例。。),等水平进一步提高后再回来补qwq。

代码实现

//
// Created by vv123 on 2022/4/9.
//
//modified on 2022/4/19:
//我们是否可以这样做?
//把颜色离散化,并二分预处理出每一种颜色不能匹配的L和R,则匹配数为sum_all-sum[L,R]。
//这样,我们需要对每一个新增父节点维护一个树状数组,和一个vector。vector不需要有序。
//每次查询是把小树vector里的点扔到大bit里查询
//那直接把小vector的颜色加到大树状数组上,同时编号push_back到大vector上,可以知道这个过程的复杂度是nlogn的。
//我们可以把每一个树上结点写成一个结构体,里面有一个指针指向vector,另一个指向BIT(逐渐离谱)
//我的数据结构水平十分有限,目前暂时无法AC本题(但没想到竟然可以过样例((),等水平进一步提高后再回来补qwq
#include <bits/stdc++.h>
using namespace std;
const int N = 2e6 + 10;

struct Edge {
    int u, v, w;
    bool operator < (const Edge& x) const {
        return w < x.w;
    }
} e[N];

vector<int> G[N];
int n, m, L, cnt, fa[N], d[N];
long long ans = 0;
struct node{
    int color, l, r, pos;
    bool operator < (const node& x) const {
        return color < x.color;
    }
    bool operator < (const int& x) const {
        return color < x;
    }
} c[N];
struct BIT {
    int t[N] = {0};
    void add(int i, int x) {
        for (; i <= n; i += i & -i)
            t[i] += x;
    }
    int sum(int i) {
        int res = 0;
        for (; i; i -= i & -i)
            res += t[i];
        return res;
    }
};
struct treenode {
    int val;
    vector<int>* v;
    BIT* T;
}a[N];

int find(int x) { return x == fa[x] ? fa[x] : fa[x] = find(fa[x]); }

treenode merge(int x, int y, int w) {
    treenode res;
    res.val = w;
    if (a[x].v->size() > a[y].v->size())
        swap(x, y);
    treenode &s = a[x], &t = a[y];
    int siz = t.v->size();
    for (auto i : *s.v) {
        if (i > n) continue;
        ans += w * (siz - (t.T->sum(c[i].r) - t.T->sum(c[i].l-1)));
    }
    for (auto i : *s.v) {
        t.v->push_back(i);
        t.T->add(c[i].color, 1);
    }
    res.T = t.T;
    res.v = t.v;
    return res;
}

void kruskal() {
    sort(e + 1, e + 1 + m);
    for (int i = 1; i <= m; i++) {
        int fu = find(e[i].u), fv = find(e[i].v);
        if (fu != fv) {
            a[++cnt] = merge(fu, fv, e[i].w);
            fa[fu] = fa[fv] = cnt;
            G[cnt].push_back(fu); G[fu].push_back(cnt);
            G[cnt].push_back(fv); G[fv].push_back(cnt);
            if (cnt - n == n - 1) break;
        }
    }
}

int main() {
    scanf("%d%d%d", &n, &m, &L);
    cnt = n;    //重构树点数
    for (int i = 1; i <= n * 2; i++) fa[i] = i;
    for (int i = 1; i <= n; i++) {
        scanf("%d", &c[i].color);
        c[i].pos = i;
    }
    for (int i = 1; i <= m; i++) {
        scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].w);
    }
    //----------处理c[i]--------------
    sort(c + 1, c + 1 + n);
    for (int i = 1; i <= n; i++) {
        c[i].l = lower_bound(c + 1, c + 1 + n, c[i].color - L) - c + 1;
        c[i].r = lower_bound(c + 1, c + 1 + n, c[i].color + L) - c - 1;//[l,r]表示半径为r的开邻域内的颜色,这部分是不被匹配的
    }
    for (int i = 1; i <= n; i++) {
        c[i].color = i;
    }
    sort(c + 1, c + 1 + n, [&](node a, node b) { return a.pos < b.pos; });
    //----------初始化树的叶节点---------
    for (int i = 1; i <= n; i++) {
        a[i].v = new vector<int>;
        a[i].v->push_back(i);
        a[i].T = new BIT;
        a[i].T->add(c[i].color, 1);
    }
    kruskal();
    cout << ans << endl;
    return 0;
}


C-魔法少女 解题报告

题目大意

给一个n行m列矩阵,每个元素为\/,代表这个点可以走的方向。问至少改变几个元素可以从右下角走到左上角,无解输出NO SOLUTION\(n , m \le 500\)

解题思路

这是一个n*m网格构成的图,总共有(n+1)*(m+1)个点。设每个对角线连接的两个点之间有一条长度为0的边,并将另一种对角线相连的两个点边权设为1,用dijkstra求(1,1)到(n+1,m+1)的最短路即可。

考虑一个方格的四个顶点

1 2

3 4

如果我们从1走到了4,就一定不会经过(2,3)这条边:

我们考虑是否可以从4走回2:

只有移动2k次才能使横向位移delta x为0,然而移动2k次一定不会使delta y为1,我们设其中x次向上走,则delta y = x + (-1) * (2k - x) = 2(x-k)一定是偶数。

同理从4也不可能走到3。

因此我们在图上跑一遍最短路,一定不会同时经过两种对角线,得到的答案就是最少需要改变几条对角线。

图上的边数为M = 2*n*m <= 5e5,使用堆优化的Dijkstra算法复杂度为O(MlogN),可以通过

代码实现

//
// Created by vv123 on 2022/4/12.
//
//我们可以把每一个对角线当成它所连接的两个点之间有一条长度为0的路径
//自然,另一种对角线的路径设为1,用dijkstra求(1,1)到(n+1,m+1)的最短路即可
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
const int INF = 0x3f3f3f3f;

struct Edge { int v, w; };
vector<Edge> G[N];
inline void addEdge(int u, int v, int w) {
    G[u].push_back({v, w});
    G[v].push_back({u, w});
}
int n, m, s, d[N], vis[N];
//我们将二维中的点(i,j)标号为(m + 1) * (i - 1) + j
inline int mark(int i, int j) {
    return (m + 1) * (i - 1) + j;
}

inline void dijkstra(int s) {
    priority_queue<pair<int, int> > q;
    memset(d, 0x3f, sizeof d);
    d[s] = 0;
    q.push({0, s});
    while (!q.empty()) {
        int u = q.top().second; q.pop();
        if (vis[u]) continue;
        vis[u] = 1;
        for (Edge& e: G[u]) {
            int v = e.v, w = e.w;
            if (d[v] > d[u] + w) d[v] = d[u] + w, q.push({-d[v], v});
        }
    }
}
/*
HACK
3 4
\\/\
\/\/
//\\
*/
int main() {
    cin >> n >> m;
    char s[N];
    for (int i = 1; i <= n; i++) {
        cin >> s + 1;
        //cout << s + 1 << endl;
        for (int j = 1; j <= m; j++) {
            //cout << s[j] << endl;
            //printf("%d-%d:%d\n", mark(i, j), mark(i + 1, j + 1), s[j] != '\\');
            //printf("%d-%d:%d\n", mark(i + 1, j), mark(i, j + 1), s[j] != '/');
            addEdge(mark(i, j), mark(i + 1, j + 1), s[j] != '\\');
            addEdge(mark(i + 1, j), mark(i, j + 1), s[j] != '/');
        }
    }
    dijkstra(mark(1, 1));
    if (d[mark(n + 1, m + 1)] >= INF) puts("NO SOLUTION");
    else cout << d[mark(n + 1, m + 1)] << endl;
    return 0;
}

E-修道路 解题报告

题目大意

image-20220422105242689

image-20220422105308009

解题思路

首先考虑如果图就是一棵树,显然此时路径唯一,因此只需dfs一遍即可。

点双有性质:任意两点间都存在至少两条简单路径,并且这两条路径不经过相同的点。

因此如果1到x的路径上有点双,设点双内权值最小的点为T,则一定存在1->...->割点1->T->割点2->..x的简单路径(如果某一条割点1->T的路径与T->割点2的路径有交点,根据性质可以换一条没有交点的路)。因此点双的点权可以用点双内的最小点权代替。(当x在点双内,显然也有T->x的路径)

对于一般的图,我们考虑求出所有割点和点双联通分量,将点双缩成一个点。我们对每一个割点向它所属的点双连边,则原图变成了一个由割点和点双构成的树。

我们从1号点所在的点dfs一遍整棵树,即可得到所有点双的答案。

代码实现

/
// Created by vv123 on 2022/4/21.
//
#include <bits/stdc++.h>
using namespace std;

const int N = 1e6 + 10;
vector<int> G[N], dcc[N];
int n, m, q, w[N], dfs_clock, dfn[N], low[N], dcc_cnt, iscut[N], dccno[N], root;
stack<int> s;

vector<int> nG[N];
int nw[N];
struct Edge {
    int u, v;
} e[N];

void tarjan(int u) {
    dfn[u] = low[u] = ++dfs_clock;
    s.push(u);
    /*
    if (u == root && G[u].empty()) {
        dcc_cnt++;
        dcc[dcc_cnt].push_back(u);
        return;
    }
     */
    int cnt = 0;//分支数量
    for (auto v : G[u]) {
        if (!dfn[v]) {
            tarjan(v);
            low[u] = min(low[u], low[v]);
            if (dfn[u] <= low[v]) {
                //printf("%d-%d\n", u, v);
                cnt++;
                if (u != root || cnt > 1) iscut[u] = 1;
                dcc_cnt++;
                int x;
                do {
                    x = s.top(); s.pop();
                    dccno[x] = dcc_cnt;
                    dcc[dcc_cnt].push_back(x);
                    nw[dcc_cnt] = min(nw[dcc_cnt], w[x]);
                } while (x != v);
                dccno[u] = dcc_cnt;
                dcc[dcc_cnt].push_back(u);
                nw[dcc_cnt] = min(nw[dcc_cnt], w[u]);
            }
        } else low[u] = min(low[u], dfn[v]);
    }
    //printf("%d dfn=%d low=%d\n", u, dfn[u], low[u]);
}

int vis[N], ans[N];
void dfs(int u, int minw) {
    vis[u] = 1;
    ans[u] = minw;
    //printf("->%d, minw = %d\n", u, minw);
    for (auto v:nG[u]) {
        if (!vis[v]) dfs(v, min(minw, nw[v]));
    }
}
/*
7 6 7
-1 -2 -1 -3 -2 -4 -2 -5 -3 -6 -3 -7
7 6 5 4 3 2 1
1 2 3 4 5 6 7
 */

void print_dcc_content() {
    for (int i = 1; i <= dcc_cnt; i++) {
        cout << i << ":";
        for (auto u:dcc[i])
            cout << u << " ";
        printf("nw=%d\n", nw[i]);
    }
}

void print_dccno() {
    for (int i = 1; i <= n; i++)
        cout << dccno[i] << " ";
    cout << endl;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    memset(nw, 0x3f, sizeof nw);
    cin >> n >> m >> q;
    for (int i = 1; i <= m; i++) {
        int u, v;
        cin >> u >> v;
        u = -u; v = -v;
        //e[i] = {u, v};
        G[u].push_back(v); G[v].push_back(u);
    }
    for (int i = 1; i <= n; i++)
        cin >> w[i];
    root = 1;
    tarjan(1);

    for (int i = 1; i <= n; i++) {
        if (iscut[i]) {
            dcc_cnt++;
            dccno[i] = dcc_cnt;
            dcc[dcc_cnt].push_back(i);
            nw[dcc_cnt] = min(nw[dcc_cnt], w[i]);
        }
    }

    for (int i = 1; i <= dcc_cnt; i++) {
        for (auto u:dcc[i]) {
            if (iscut[u]) {
                nG[dccno[u]].push_back(i);
                nG[i].push_back(dccno[u]);
            }
        }
    }
    //缩点后的图由 割点 和 点双 两部分组成
    //点双有性质:任意两点间都存在至少两条简单路径,并且这两条路径不经过相同的点。
    //print_dcc_content();
    //print_dccno();
    /*
    for (int i = 1; i <= m; i++) {
        int u = dccno[e[i].u], v = dccno[e[i].v];
        nG[u].push_back(v); nG[v].push_back(u);
    }
    */
    dfs(dccno[1], nw[dccno[1]]);

    while (q--) {
        int t;
        cin >> t;
        if (t == 1) cout << w[1] << "\n";
        else cout << ans[dccno[t]] << "\n";
    }
    return 0;
}

F-开会了 解题报告

题目大意

a是有n个元素的整数数组,有m 个形如\(a_{u}-a_{v}\le w\)的约束条件。

请判断是否存在a满足上述条件。\(n,m \le 5 \times 10^{3},-10^{4}<w<10^{4}\)

解题思路

设约束条件为u->v长度为w的有向边,显然,如果构成的图是链和树,可以给根节点一个初值然后依次取等号推出所有的点。

如果有1->2;1->3->2这样的环,我们可以给a2一个初值推出a3再推出a1,和a2直接推出的a1取一个最小值即可,不会发生矛盾。

问题的关键在于图中的有向环。

我们考虑1->2->3->1的一个环,

得到三个不等式a1<=a2+w1,a2<=a3+w2,a3<=a1+w3

我们将三个不等式相加,得到w1+w2+w3>=0

也就是说,如果w1+w2+w3<0,无论a1~a3如何取值都不能满足条件。否则也可以通过定一议二的方法构造出解。

因此只需判断图上是否有负环即可。如果在BellmanFord中访问一个点超过n次,说明一定存在负环。

代码实现

//
// Created by vv123 on 2022/4/10.
//

#include <bits/stdc++.h>
using namespace std;
const int N = 5e3 + 10;
struct Edge {
    int u, v, w;
};
struct BellmanFord {
    int n, m;
    vector<Edge> edges;
    vector<int> G[N];
    void AddEdge(int u, int v, int w) {
        edges.push_back({u, v, w});
        G[u].push_back(edges.size() - 1);
    }

    int d[N], p[N], cnt[N], inq[N];
    bool negativeCycle() {
        queue<int> Q;
        for (int i = 1; i <= n; i++) Q.push(i);
        d[0] = 0;
        while (!Q.empty()) {
            int u = Q.front(); Q.pop();
            inq[u] = 0;
            for (int i = 0; i < G[u].size(); i++) {
                Edge& e = edges[G[u][i]];
                int v = e.v, w = e.w;
                if (d[v] > d[u] + w) {
                    d[v] = d[u] + w;
                    p[v] = G[u][i];
                    if (!inq[v]) {
                        Q.push(v);
                        inq[v] = 1;
                        if (++cnt[v] > n) return true;
                    }
                }
            }
        }
        return false;
    }
};

int main() {
    BellmanFord bf;
    cin >> bf.n >> bf.m;
    for (int i = 1; i <= bf.m; i++) {
        int u, v, w;
        cin >> u >> v >> w;
        bf.AddEdge(u, v, w);
    }
    bf.negativeCycle() ? puts("NO") : puts("YES");
    return 0;
}

G-二分图匹配 解题报告

题目大意

image-20220423183446793

解题思路

固定c数组,d数组有n!种排列,而考虑一对d[j]>c[i]的贡献,它会出现在(n-1)!种排列中。

因此一对d[j]>c[i]对期望的贡献恰好为w除以n,对答案的贡献恰好为w[i]。

我们考虑一条匹配边(x,y),设权值为满足a[x] + b[y]>w的w的和,可以前缀和+二分求出这个边权,则边权和即为答案。原问题转化为二分图的最大权完美匹配问题。

定义可行顶标: 函数l ,s.t.对任意弧(x,y)都有l(x)+l(y)>=w(x,y)

相等子图:只保留满足l(x)+l(y)=w(x,y)的边的子图

定理:如果相等子图有完美匹配,则该匹配是原图的最大权匹配

证明:假设我们找到了相等子图的一个完美匹配,此时的可行顶标为l

任取原图的一个最大权匹配,一定有有边权和<=顶标和

而这个相等子图的完美匹配,满足边权和==顶标和>=最大权匹配的边权和

因而它只能是最大权匹配

问题的关键在于找到合适的l。

设S,T表示左/右在交错树中的点,不在交错树中的点各自为S',T'
考虑每次对S和T的lx -= k, ly += k,则S-T'间的l(x)+l(y)减小k,可能加入相等子图,其他集合间的边没有变化。
显然,选k=min(lx+ly-w)。

我们可以对T中的点维护slack(v)=min(lx+ly-w, u属于S),并取最小的slack(v)为k。当找到完美匹配时,算法终止。

代码实现

//
// Created by vv123 on 2022/4/22.
//
#include <bits/stdc++.h>
#define clr(arr) memset(arr,0,sizeof arr)
#define inf(arr) memset(arr, 0x3f, sizeof arr)
#define int long long
using namespace std;

const int N = 410;
const int INF = 2e18 + 10;
int n, m, a[N], b[N], d[N], ans, sum[N], val[N];
int lx[N], ly[N], vis[N], slack[N], match[N], pre[N];

struct node {
    int c, w;
    bool operator < (const node& x) const {
        return c < x.c;
    }
    bool operator < (const int x) const {
        return c < x;
    }
} e[N];

int w(int x, int y) {
    int pos = lower_bound(e + 1, e + 1 + n, a[x] + b[y]) - e - 1;//printf("%d<->%d: %d\n", x, y, sum[pos]);
    return sum[pos];
}

void find(int s) {
    //可行顶标:l,s.t.对任意弧(x,y)都有l(x)+l(y)>=w(x,y)
    //相等子图:只保留满足l(x)+l(y)=w(x,y)的边的子图
    //定理:如果相等子图有完美匹配,则该匹配是原图的最大权匹配
    //证明:假设我们找到了相等子图的一个完美匹配,此时的可行顶标为l
    //任取原图的一个最大权匹配,一定有有边权和<=顶标和
    //而这个相等子图的完美匹配,满足边权和==顶标和>=最大权匹配的边权和
    //因而它只能是最大权匹配
    //问题的关键在于找到合适的l。
    //设S,T表示左/右在交错树中的点,各自补集为S',T'
    //考虑每次对S和T的lx -= k, ly += k,则S-T'间的l(x)+l(y)减小k,可能加入相等子图。
    //显然,选k=min(lx+ly-w)。我们可以对T中的点维护slack(v)=min(lx+ly-w, u in S),并取最小的slack(v)为k
    clr(vis), clr(pre), inf(slack);
    int y = 0, miny = 0, k;
    match[y] = s;
    for (;;) {
        k = INF; vis[y] = 1;
        for (int i = 1; i <= n; i++) {
            if (!vis[i]) {
                if (slack[i] > lx[match[y]] + ly[i] - w(match[y], i))
                    slack[i] = lx[match[y]] + ly[i] - w(match[y], i), pre[i] = y;
                if (slack[i] < k)
                    k = slack[i], miny = i;
            }
        }
        for (int i = 0; i <= n; i++) {
            if (vis[i]) lx[match[i]] -= k, ly[i] += k;
            else slack[i] -= k;
        }
        y = miny;
        if (!match[y]) break;
    }
    while(y) { match[y] = match[pre[y]]; y = pre[y]; }
}

void KM() {
    clr(match); clr(lx); clr(ly);
    for (int i = 1; i <= n; i++)
        find(i);
}


signed main() {
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> e[i].c;
    for (int i = 1; i <= n; i++) cin >> e[i].w;
    for (int i = 1; i <= n; i++) cin >> a[i];
    for (int i = 1; i <= n; i++) cin >> b[i];

    sort(e + 1, e + 1 + n);
    for (int i = 1; i <= n; i++)
        sum[i] = sum[i - 1] + e[i].w;//printf("sum%d=%d\n", i, sum[i]);

    KM();

    for (int i = 1; i <= n; i++)
        ans += w(match[i], i);
    cout << ans << endl;
    return 0;
}

H-守护最好的0 解题报告

题目大意

image-20220422113851396

\(n,m \le 1 \times 10^{6}\)

解题思路

可证(cai)结论:n为偶数时不删去任何边,n为奇数时可以仅删除一点,此时包含最优解。

我们按点权从小到大枚举,如果一个点不是割点,或者删去该点后不出现奇数连通块,就删这个点即可,即把这个点变为孤立点,答案为 总点权 - 2 * 该点点权。(这是因为:如果删去一个割点后出现了奇数连通块,必然还要删别的非割点,一定不是最优解)

我们可以在dfs过程中初始化siz[u]=1,并对每个子节点v,siz[u] += siz[v]。

如果某割点存在一个子节点的siz[v]为奇数,则标记为“不被选择”。

关于这一点,我在解题时一开始还判断了n-siz[u](被我当成“剩下的连通块”)是不是奇数,但这样会wrong answer。

后来群里李尚融同学的发言让我恍然大悟:

当我们判断n-siz[u]的时候,没有包含u的子树中不以u为割点(即:仍然会连回祖先)的连通块,因此实际上删去u点后产生的连通块除了以u为割点的连通块以外,剩下的一个大于n-siz[u],所以这样是错的。

实际上,只需要判断u的子树中以u为割点的连通块是不是偶数,假设这些连通块的总大小为S,则剩下那一个连通块的大小就是n-S-1,如果n是奇数且S是偶数,n-S-1自然也是偶数,因此是不需要判断的。感谢李尚融大佬!

下面是关键结论的证明,我就不再打一遍了。

首先可以证明,答案一定是一些孤立点组成,即最后分成的若干连通块中没有点数大于等于3的奇数点数连通块。否则,可以在这些个奇数连通块上再割下一个非割点,使答案变大

其次,剩下的孤立点的个数一定是奇数个。否则,由于除了孤立点剩下的所有连通块都是偶数个点,从而推出n为偶数(矛盾)。

因此答案仅会出现在删除奇数个孤立点。

下面假设孤立点的个数大于等于3。

首先,这些点一定都是原图的割点。否则,如果其中存在一个非割点,则将具割去,分成一个孤立点和一个偶数的连通块,答案更大。

现在如果在原图中把这些点单独割去,那么剩下的连通块中一定存在至少两个连通块,点数为奇数(此时点数不一定大于等于3)。否则,可以仅需要单独割掉其中一个,答案会更优。而由于这些个连通块都是奇数个点,于是还需要在里面继续找割点并重复上面的操作。

因此在上述假设前提下答案必为原图割点,且剩下的奇数连通块需要再次进行找割点的操作。

接下来,我们将割下来的孤立点放在点双树上,建立一棵虚树(不知道什么是点双树可以搜索圆方树,两者差不多是同一个东西),那么由于割点至少在两个方向上都有其他割点,但按照上述过程,存在孤立点必为叶子,即不可能在两个方向上都有其他割点.否则,这棵树就存在一个环。但一棵树怎么可能有环呢?假设不成立!

于是,结合第一页的证明,也就说明答案仅会出现在删除一个点上。

(来自ppt)

代码实现

//
// Created by vv123 on 2022/4/21.
//
#include <bits/stdc++.h>
using namespace std;

const int N = 2e6 + 10;
const int INF = 0x3f3f3f3f;
vector<int> G[N], dcc[N];
int n, m, dfs_clock, dfn[N], low[N], dcc_cnt, iscut[N], dccno[N], root;
stack<int> s;

vector<int> nG[N];
int nw[N];
struct Edge {
    int u, v;
} e[N];

int siz[N], ok[N];

void tarjan(int u) {
    siz[u] = 1;
    dfn[u] = low[u] = ++dfs_clock;
    s.push(u);
    /*
    if (u == root && G[u].empty()) {
        dcc_cnt++;
        dcc[dcc_cnt].push_back(u);
        return;
    }
     */
    int cnt = 0;//分支数量
    for (auto v : G[u]) {
        if (!dfn[v]) {
            tarjan(v);
            siz[u] += siz[v];
            low[u] = min(low[u], low[v]);
            if (dfn[u] <= low[v]) {
                //printf("%d-%d\n", u, v);
                if (siz[v] % 2 == 1) ok[u] = 0;
                cnt++;
                if (u != root || cnt > 1) iscut[u] = 1;
                dcc_cnt++;
                int x;
                do {
                    x = s.top(); s.pop();
                    dccno[x] = dcc_cnt;
                    dcc[dcc_cnt].push_back(x);
                } while (x != v);
                dccno[u] = dcc_cnt;
                dcc[dcc_cnt].push_back(u);
            }
        } else low[u] = min(low[u], dfn[v]);
    }
    //if ((n - siz[u]) % 2 == 1) ok[u] = 0;
    //printf("%d dfn=%d low=%d\n", u, dfn[u], low[u]);
}


/*
7 6 7
-1 -2 -1 -3 -2 -4 -2 -5 -3 -6 -3 -7
7 6 5 4 3 2 1
1 2 3 4 5 6 7
1 7 6
1 2 3 4 5 6 7
1 2 1 3 2 4 2 5 3 6 3 7
 */

void print_dcc_content() {
    for (int i = 1; i <= dcc_cnt; i++) {
        cout << i << ":";
        for (auto u:dcc[i])
            cout << u << " ";
        printf("nw=%d\n", nw[i]);
    }
}

void print_dccno() {
    for (int i = 1; i <= n; i++)
        cout << dccno[i] << " ";
    cout << endl;
}

void print_size() {
    for (int i = 1; i <= n; i++)
        cout << siz[i] << " ";
    cout << endl;
}

struct node {
    int w, id;
}a[N];

bool cmp(node x, node y) {
    if (x.w != y.w) return x.w < y.w;
    if (iscut[x.id] != iscut[y.id]) return iscut[x.id] < iscut[y.id];
    return x.id < y.id;
}

void init() {
    dfs_clock = dcc_cnt = 0;
    for (int i = 1; i <= n; i++) {
        ok[i] = 1;
        G[i].clear();
        dcc[i].clear();
        dfn[i] = iscut[i] = dccno[i] = 0;
    }
    while (!s.empty()) s.pop();
}


int main() {
    //ios::sync_with_stdio(false);
    //cin.tie(0);
    //cout.tie(0);
    int T;
    //cin >> T;
    scanf("%d", &T);
    while (T--) {
        //cin >> n >> m;
        scanf("%d%d", &n, &m);
        init();
        long long ans = 0;
        for (int i = 1; i <= n; i++) {
            scanf("%d", &a[i].w);
            ans += a[i].w;
            a[i].id = i;
        }
        for (int i = 1; i <= m; i++) {
            int u, v;
            scanf("%d%d", &u, &v);
            G[u].push_back(v); G[v].push_back(u);
        }

        if (n % 2 == 0) {
            printf("%lld\n", ans);
            continue;
        }

        root = 1;
        tarjan(1);
        //print_size();


        sort(a + 1, a + 1 + n, cmp);
        for (int i = 1; i <= n; i++) {
            if (!iscut[a[i].id] || (iscut[a[i].id] && ok[a[i].id])) {
                printf("%lld\n", ans - 2 * a[i].w);
                //cout << ans - 2 * a[i].w << endl;
                break;
            }
        }
    }
    return 0;
}

I-又开会了 解题报告

题目大意

给出m条形如a[u] == x || a[v] == y的条件,问是否可以同时成立。

\(n,m \le 1 \times 10^{5}\)

解题思路

这是2-SAT问题的模板题。

构造一张有向图G,其中每个变量xi 拆成两个节点2i和2i+1,分别表示xi为假和xi为真。最后要为每一个变量选其中的一个节点标记。比如,若标记了节点2i+1->2j,如果标记节点2i+1那么也必须标记节点j(因为如果xi为真,则xj必须满足为假才能使条件成立)。这条有向边相当于”推导出“的意思,同理,还需要连一条有向边2j+1->2i 。对于其他的情况,也可以类似连边。换句话说,每个条件对应两条”对称“的边。

接下来逐一考虑每个没有赋值的变量,设为xi。我们先假设它为假,然后标记节点2i,并且沿着有向边标记所有能标记的节点。如果标记过程中发现某个变量对应的两个节点都被标记,则“xi“为假这个假设不成立,需要改成“xi为真”,然后重新标记。注意,这个算法没有回溯过程。如果当前考虑的变量不管赋值为真还是假都会引起矛盾,可以证明整个2-SAT问题无解(即调整以前赋值的其他变量也没用)

代码实现

//
// Created by vv123 on 2022/4/10.
//
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;

class TwoSAT {
    int n, m;
    vector<int> G[N << 1];
    int S[N << 1], c;
    bool mark[N << 1];
public:
    TwoSAT(int N, int M) : n(N), m(M) {}
    bool dfs(int u) {
        if (mark[u^1]) return false;
        if (mark[u]) return true;
        mark[u] = 1;
        S[c++] = u;
        for (auto v:G[u])
            if (!dfs(v)) return false;
        return true;
    }

    void AddORClause(int u, int x, int v, int y) {
        u = u << 1 | x, v = v << 1 | y;
        G[u^1].push_back(v); G[v^1].push_back(u);
    }

    bool solve() {
        for (int i = 0; i < n * 2; i += 2) {
            if (!mark[i] && !mark[i + 1]) {
                c = 0;
                if (!dfs(i)) {
                    while (c > 0) mark[S[--c]] = 0;
                    if (!dfs(i + 1)) return false;
                }
            }
        }
        return true;
    }
};

int main() {
    int n, m;
    cin >> n >> m;
    TwoSAT *ts = new TwoSAT(n, m);
    for (int i = 1; i <= m; i++) {
        int u, x, v, y;
        cin >> u >> x >> v >> y;
        ts->AddORClause(u - 1, x, v - 1, y);   //下标从0开始
    }
    ts->solve() ? puts("YES") : puts("NO");
    return 0;
}

J-tarjan 解题报告

题目大意

给一个无向图,求割点数量,割边数量,极大点双连通分量数量,极大点双连通分量包含边数的最大值。

\(n \le 1000\)

解题思路

这是无向图tarjan算法的模板题。

对于无向图G(方便起见只考虑联通图),如果删除某个点u后,连通分量数目增加,称u为图的关节点或割顶,对于连通图,割顶就是删除之后使图不再联通的点。对于连通图,删除一条边(u,v)后使图不在联通,则称(u,v)是桥。
定理: 在无向图G的dfs树中,非根结点u是G的割顶当且仅当u存在一个子节点v使得v及其后代都没有连回u祖先(u不算)的边。
可以类推出, 在无向图G的dfs树中,(u,v)是桥当且仅当v及其v的的后代除了无向边(u,v)以外不存在连回u及其u祖先的边。

割点low[v]>=pre[u] 时u为割点,并产生新的点双

割边low[v]>pre[u] 时(u,v)为割边

我们可以用一个栈存下当前bcc中的边。

代码实现

//
// Created by vv123 on 2022/4/11.
//
#include <bits/stdc++.h>
using namespace std;
const int N = 2e3 + 10;
vector<int> G[N], bcc[N];
int n, m, dfs_clock, cut_cnt, bridge_cnt, bcc_cnt, bcc_maxsize;
int pre[N], post[N], bccno[N], iscut[N], isbridge[N][N];

struct Edge { int u, v; };
stack<Edge> S;

int dfs(int u, int fa) {
    int lowu = pre[u] = ++dfs_clock, child = 0;
    for (auto v:G[u]) {
        Edge e = {u, v};
        if (!pre[v]) {
            S.push(e); child++;
            int lowv = dfs(v, u);
            lowu = min(lowu, lowv);
            if (lowv >= pre[u]) {
                iscut[u] = true; //cut_cnt++;是错误的, 因为一个点会被多次判定为割点,割边同理。
                if (lowv > pre[u]) isbridge[u][v] = isbridge[v][u] = true;
                bcc_cnt++; bcc[bcc_cnt].clear();
                int edge_cnt = 0;
                for(;;) {
                    Edge x = S.top(); S.pop(); edge_cnt++;
                    if (bccno[x.u] != bcc_cnt)
                        bccno[x.u] = bcc_cnt, bcc[bcc_cnt].push_back(x.u);
                    if (bccno[x.v] != bcc_cnt)
                        bccno[x.v] = bcc_cnt, bcc[bcc_cnt].push_back(x.v);
                    if (x.u == u && x.v == v) {
                        bcc_maxsize = max(bcc_maxsize, edge_cnt);
                        break;
                    }
                }
            }
        } else if (pre[v] < pre[u] && v != fa) {
            S.push(e);
            lowu = min(lowu, pre[v]);
        }
    }
    if (fa < 0 && child == 1) iscut[u] = 0;
    return lowu;
}

inline void find_bcc() {
    dfs_clock = bcc_cnt = 0;
    for (int i = 1; i <= n; i++)
        if (!pre[i]) dfs(i, -1);
}

int main() {
    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        int u, v;
        cin >> u >> v;
        G[u].push_back(v);
        G[v].push_back(u);
    }
    find_bcc();
    for (int i = 1; i <= n; i++)
        if (iscut[i]) cut_cnt++;
    for (int i = 1; i <= n; i++)
        for (int j = i + 1; j <= n; j++)
            if (isbridge[i][j]) bridge_cnt++;
    printf("%d %d %d %d\n", cut_cnt, bridge_cnt, bcc_cnt, bcc_maxsize);
    return 0;
}

K-居民们都住在房屋里 解题报告

题目大意

m次询问树上两点之间的最短路径长度

\(n,m \le 5 \times10^{5}\)

解题思路

这是LCA的模板题。

我们在dfs的过程中求出结点深度,并利用倍增思想求出f的2^i级祖先。

查询两个点的LCA时,设深度差为k,较深的点为x,我们可以在k的二进制为1的位置进行x = f[x][i],跳到同一深度后,如果两点不重合就同步上跳直至重合在LCA。

树上两点之间的最短路径必然经过LCA,长度为d[u] + d[v] - 2 * d[lca(u, v)]

代码实现

//
// Created by vv123 on 2022/4/10.
//
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;

int f[N][30], d[N], n, q;
vector<int> G[N];

void dfs(int u, int fa) {
    d[u] = d[fa] + 1; f[u][0] = fa;
    for (int i = 1; (1 << i) < d[u]; i++)
        f[u][i] = f[f[u][i-1]][i-1];
    for (auto v:G[u])
        if (v != fa) dfs(v, u);
}

int lca(int x, int y) {
    if (d[x] < d[y]) swap(x, y);
    int k = d[x] - d[y];
    for (int i = 25; i >= 0; i--)
        if ((1 << i) & k) x = f[x][i];
    if (x == y) return x;
    for (int i = 25; i >= 0; i--)
        if (f[x][i] != f[y][i]) x = f[x][i], y = f[y][i];
    return f[x][0];
}

int main() {
    cin >> n >> q;
    for (int i = 1; i <= n - 1; i++) {
        int u, v;
        cin >> u >> v;
        G[u].push_back(v);
        G[v].push_back(u);
    }
    dfs(1, 0);
    while (q--) {
        int u, v;
        cin >> u >> v;
        cout << d[u] + d[v] - 2 * d[lca(u, v)] << endl;
    }
    return 0;
}

L-最小生成树 解题报告

题目大意

求图中最小生成树的个数

\(n \le 100,m \le 1000,w \le 10\)

解题思路

首先用kruskal求一遍生成树,如果求不出来输出0并退出。接下来计算方案数:

引理1:MST上每种权值的边数是一定的。

可以这样考虑:根据Kruskal算法的流程,如果在求好的MST上,权值为x的边还能再多一个,那一定会在求的过程中算入这条边。

引理2:不同的生成树中某一种权值的边连接完成后,形成的联通块状态是一样的。

可以这样考虑:我们可以把n选m等效为连上n个后拆掉(n-m)个使图中无环,但这不会改变联通性。

因此,“连接每种权值的边”是一个相对独立的过程,我们可以分别对每种权值的边计算方案数。

引理3:矩阵树定理:基尔霍夫矩阵K=D-A的任意一个余子式的行列式等于生成树个数,D是度数矩阵,A是邻接矩阵
因此,对每种w,我们把断开权值为w的边剩下的连通块缩成点,然后用所有权值w的边连接,利用Matrix Tree算法求出生成树个数,答案即为所有w对应的结果相乘。

注意到w的值域很小,枚举所有被使用的边权w,使用\(O(N^3)\)的Gauss消元行列式最多算十次,是可以接受的。

代码实现

//
// Created by vv123 on 2022/4/11.
//
//引理1:MST上每种权值的边数是一定的。可以这样考虑:根据Kruskal算法的流程,如果在求好的MST上,权值为x的边还能再多一个,那一定会在求的过程中算入这条边。
//引理2:不同的生成树中某一种权值的边连接完成后,形成的联通块状态是一样的。可以这样考虑:我们可以把n选m等效为连上n个后拆掉(n-m)个使图中无环,但这不会改变联通性。
//因此,“连接每种权值的边”是一个相对独立的过程,我们可以分别对每种权值的边计算方案数。
//引理3:矩阵树定理:基尔霍夫矩阵K=D-A的任意一个余子式的行列式等于生成树个数,D是度数矩阵,A是邻接矩阵
//因此,对每种w,我们断开MST上所有权值w的边,把剩下的连通块当作点,然后连上所有权值w的边,利用Matrix Tree算法求出生成树个数,答案即为所有w对应的结果相乘。注意到w的值域很小,行列式最多算十次,是可以接受的。
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 110;
const int M = 1010;
const int mod = 10000;

int n, m, f[N], id[N], cnt, used[M], a[N][N], used_w[11];
long long ans = 1;

void init() { for (int i = 1; i <= n; i++) f[i] = i; }
int find(int x) { return f[x] == x ? x : f[x] = find(f[x]); }
int merge(int x, int y) { return find(x) == find(y) ? 0 : f[find(x)] = find(y);}

struct Edge {
    int u, v, w;
    bool operator < (const Edge& x) const {
        return w < x.w;
    }
} e[M];

bool kruskal() {
    init(); int cnt = 0;
    sort(e + 1, e + 1 + m);
    for (int i = 1; i <= m; i++) {
        if (merge(e[i].u, e[i].v)) {
            cnt++;
            used[i] = 1;
            used_w[e[i].w] = 1;
        }
        if (cnt == n - 1) break;
    }
    return cnt == n - 1;
}

inline int det(int n) {
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            a[i][j] %= mod;
    int res = 1;
    for (int i = 1; i <= n; i++) {
        int p = i;
        for (int j = i + 1; j <= n; j++)
            if (abs(a[j][i])) p = j;
        for (int j = 1; j <= n; j++) swap(a[p][j],a[i][j]);
        if (p != i) res *= -1;
        for (int j = i + 1; j <= n; j++)
            while (a[j][i]) {
                int t = a[j][i] / a[i][i];
                for (int k = 1; k <= n; k++)
                    a[j][k] = (a[j][k] - t * a[i][k]) % mod;
                if (!a[j][i]) break;
                res *= -1;
                for (int k = 1;k <= n; k++) swap(a[i][k],a[j][k]);
            }
        res = (res * a[i][i]) % mod;
    }
    return (res % mod + mod) % mod;
}

void print() {
    for (int i = 1; i <= n; i++)
        cout << find(i) << " "; puts("");
}

inline void work(int w) {
    init(); cnt = 0;
    for (int i = 1; i <= m; i++)
        if (used[i] && e[i].w != w)
            merge(e[i].u, e[i].v);
    for (int i = 1; i <= n; i++)
        if (find(i) == i)
            id[i] = ++cnt;
    for (int i = 1; i <= n; i++)
        id[i] = id[find(i)];
    for (int i = 1; i <= m; i++) {
        if (e[i].w == w && id[e[i].u] != id[e[i].v]) {
            int u = id[e[i].u], v = id[e[i].v];
            a[u][u]++; a[v][v]++;
            a[u][v]--; a[v][u]--;
        }
    }/*
    for (int i = 1; i <= cnt; i++) {
        for (int j = 1; j <= cnt; j++)
            printf("%5d ", a[i][j]);
        puts("");
    }*/
}

int main() {
    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        int u, v, w;
        cin >> u >> v >> w;
        e[i] = {u, v, w};
    }
    if (!kruskal()) { puts("0"); return 0; }

    for (int i = 1; i <= 10; i++) {
        if (!used_w[i]) continue;
        memset(a, 0, sizeof a);
        work(i);
        ans = ans * det(cnt - 1) % mod;
    }
    cout << ans << endl;
    return 0;
}

N-摆摆国的道路修复 解题报告

题目大意

求最小直径生成树的直径

\(n \le 500\)

解题思路

求解直径最小生成树,首先需要找到 图的绝对中心图的绝对中心 可以存在于一条边上或某个结点上,该中心到所有点的最短距离的最大值最小。最小直径生成树的直径,即为绝对中心点离最远结点的距离的二倍。因此本题并不用建出最小直径生成树,只需找到图的绝对中心。

我们预处理出所有点对之间的最短路\(d(i,j)\),并求出每个结点\(i\)的第\(j\)远点\(rk(i,j)\)。这个过程可以用Floyd算法。

对于绝对中心在结点上的情况,只需要枚举所有结点用\(2 \times rk(i,n)\)更新ans即可。

对于绝对中心\(c(x)\)在边\((u,v)\)上的情况,我们设\(f(x)\)表示如果”中心点“选在当前边上离u点x单位远处,它离最远点的距离,下面我们要求出\(f(x)\)的最小值来更新ans。

我们假设\(i\)点是离\(c\)最远的点,则\(d(c(x),i)=\min(d(u,i)+x, d(v,i)+(w-x))\),

对于确定的\(i\), \(d(c(x),i)\)是一个关于x的函数,由两个斜率绝对值为1的线段构成,图像如下。

image-20220423150206548

根据\(f(x)\)的定义可知\(f(x) = \max\{ d(c(x),i)\},i \in[1,n]\),其函数是一段折线,图像如下。

image-20220423150247069

我们发现\(f(x)\)的最小值必然是\(d(c(x),i)\)\(d(c(x),j)\)的交点\((i\ne j)\)

我们枚举这些交点,即可更新\(f(x)\)的最小值。具体过程总结如下。

//观察折线图,极小值点满足对于两个点i、j,从u、v两个方向走的距离一个左小右大,一个左大右小(否则,还不如选端点)
//也就是说极小值点是在d(u,i)<d(u,j)但d(v,i)>d(v,j)时,函数y=d(v,i) + x和y=d(v,j) + w - x的交点,
//极小值=d(u,i)+x=d(v,j)+w-x
//=( d(u,i)+x + d(v,j)+w-x ) / 2
//=(d(u,i)+d(v,j)+w)/2
//于是树的直径ans可能被极小值*2=d(u,i)+d(v,j)+w更新。
//进一步观察得到,我们依次选择距离u由第n-1远到近处的i,与之相交的d(v,j)的j一定是上次相交的i(当然,j从n开始)
//因为两个交点之间上方的折线来自同一个单峰函数,只是左边来自u,右边来自v。(观察折线下方的一排平行四边形可以得到这个结论)

程序的时间复杂度取决于Floyd算法,为\(O(N^3)\)

代码实现

//
// Created by vv123 on 2022/4/11.
//
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 510;
const int INF = 1e15;
int n, m, d[N][N], rk[N][N];

struct Edge { int u, v, w; } e[N * N];

inline void Floyd() {
    for (int i = 1; i <= n; i++) d[i][i] = 0;
    for (int k = 1; k <= n; k++)
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++)
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

inline int solve() {
    Floyd();
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) rk[i][j] = j;
        sort(rk[i] + 1, rk[i] + 1 + n,  [&](int a, int b) { return d[i][a] < d[i][b]; });
    }
    int ans = INF;
    for (int i = 1; i <= n; i++) ans = min(ans, 2 * d[i][rk[i][n]]);
    for (int i = 1; i <= m; i++) {
        int u = e[i].u, v = e[i].v, w = e[i].w;
        //重点解释下面这个循环,可以看作是乱搞的结果:
        //设f(x)表示如果”中心点“选在当前边上离u点x单位远处,它离最远点的距离,这有可能成为图的“半直径”
        //我们需要求出它的最小值*2来更新ans
        //观察OI-WIKI上的折线图,极小值点满足对于两个点i、j,从u、v两个方向走的距离一个左小右大,一个左大右小(否则,还不如选端点)
        //也就是说极小值点是在d(u,i)<d(u,j)但d(v,i)>d(v,j)时,函数y=d(v,i) + x和y=d(v,j) + w - x的交点,
        //极小值=d(u,i)+x=d(v,j)+w-x
        //=( d(u,i)+x + d(v,j)+w-x ) / 2
        //=(d(u,i)+d(v,j)+w)/2
        //于是树的直径ans可能被极小值*2=d(u,i)+d(v,j)+w更新。
        //进一步观察得到,我们依次选择距离u由第n-1远到近处的i,与之相交的d(v,j)的j一定是上次相交的i(当然,j从n开始)
        //因为两个交点之间上方的折线来自同一个单峰函数,只是左边来自u,右边来自v。(观察折线下方的一排平行四边形可以得到这个结论)
        for (int j = n, i = n - 1; i >= 1; i--) {
            if (d[v][rk[u][i]] > d[v][rk[u][j]]) {
                ans = min(ans, d[u][rk[u][i]] + d[v][rk[u][j]] + w);
                j = i;
            }
        }
    }
    return ans;
}

signed main() {
    //memset(d, 0x3f, sizeof(d)); //never forget!
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            d[i][j] = INF;
    for (int i = 1; i <= m; i++) {
        int u, v, w;
        cin >> u >> v >> w;
        if (d[u][v] > w) d[u][v] = d[v][u] = w;
        e[i] = {u, v, w};
    }
    cout << solve() << endl;
    return 0;
}

O-纯白色的少年郎,如今身在何方 解题报告

题目大意

求单源最短路径,不能到达输出-1

\(n \le 1\times 10^5, m \le 2 \times 10^5\)

解题思路

单源最短路径通常使用优先队列优化Dijkstra算法。

它的基本流程是:

初始化d[s]=0,d[其它]=无穷大。每次找到未访问的结点中d值最小的结点u,这时节点的u最短路径已经确定,标记u已访问,然后枚举它连接的结点v,更新所有v的d值。重复上述操作,直到所有结点都被访问。如果此时某个结点的d值仍为无穷大,则无法到达。

优先队列优化:

”每次找到未访问的结点中d值最小的结点u“,这个过程可以用优先队列改进为,每更新一个d[v],将v扔进优先队列并按d[v]从小到大排序,每次取出队头判断一下这个点是否已被访问过即可。这样时间复杂度为\(O(M\log M)\)

代码实现

//
// Created by vv123 on 2022/4/11.
//
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e5 + 10;
const int inf = 1e15;
struct Edge { int v, w; };
vector<Edge> G[N];
int n, m, s, d[N], vis[N];
inline void solve() {
    priority_queue<pair<int, int> > q;
    for (int i = 1; i <= n; i++) d[i] = inf;
    d[s] = 0;
    q.push({0, s});
    while (!q.empty()) {
        int u = q.top().second; q.pop();
        if (vis[u]) continue;
        vis[u] = 1;
        for (Edge& e: G[u]) {
            int v = e.v, w = e.w;
            if (d[v] > d[u] + w) d[v] = d[u] + w, q.push({-d[v], v});
        }
    }
    for (int i = 1; i <= n; i++)
        if (d[i] < inf) cout << d[i] << endl;
        else cout << -1 << endl;
}

signed main() {
    cin >> n >> m >> s;
    for (int i = 1; i <= m; i++) {
        int u, v, w;
        cin >> u >> v >> w;
        G[u].push_back({v, w});
    }
    solve();
    return 0;
}

Q-建设道路 解题报告

题目大意

图上两点之间的点权为\(|i-j| \times D+A_{i}+A_{j}\) ,求最小生成树。

\(n \le 1\times 10^5\)

解题思路

如果将此图当成完全图,边数为\(O(N^2)\),显然是会超时的。我们尝试减少图中的边数。

考虑某一连续区间,将它分为左右两边,如果j在左边,i在右边,则横跨左右子区间的边权为$$ (A_{i} + i × D) + (A_{j} - j × D)$$

我们设\(wr(i)=A_{i} + i × D,wl(i)=A_{i} - i × D\),则上式简化为\(wr(i)+wl(j)\)

我们考虑有哪些边可能加入最小生成树,只把这些边相连。

结论是:连接左右子区间的边,至少有一个端点是所在区间内权值最小的点

因此我们把左子间的wl最小点和右子区间所有点连边,把右子间的wr最小点和左子区间所有点连边,即可覆盖横跨左右区间的所有情况。从[1,n]开始递归左右子区间,分治建边,即可覆盖MST上所有可能出现的边。此时边数优化为\(O(N\log N)\),可以使用kruskal算法通过本题。

下面证明这个结论:

先引入一个经典的性质:MST不可能包含一个环上的严格最大边。

我口糊了一个证明:假设我们选了环上最大边,我们加一条边就能复原这个环,则断开环上的一条边,图仍然联通,显然可以断开那个最大边,得到更小生成树

我们设在右子区间中wr最小点为a,次小点为b,左子区间中wl最小点为x,次小点为y,则a<->x<->b<->y<->a构成一个环,根据上述性质,严格最大边b<->y一定不在MST中,符合结论。

接下来讨论a=b的情况。假设x和y已经联通,那显然不会选y,符合上述结论。如果我们不通过a和b而是通过左区间内的点联通x和y,则情况没有变化。下面考虑通过a和b使x和y联通的情况:

假设a和b未联通,则需从4条边中选三条,显然可以选a<->y、a<->x、b<->x

假设a和b已联通,则显然可以选a<->y、a<->x

综上,我们选择的边至少有一个端点在所在区间内是权值最小点。

代码实现

//

// Created by vv123 on 2022/4/12.

//

//点对之间的边权为|i-j|*D+Ai+Aj

//我们考虑某一连续区间,将它分为左右两边,如果j在左边,i在右边,则左右之间连边的边权为。

//	     (Ai + i × D) + (Aj - j × D)

//假设当前左右两个子区间没有边,那么我们一定要选一条边将它们连接,这条边要如何选择呢?

//先引入一个经典的性质:MST不可能包含一个环上的严格最大边。

//大概可以这样证明:假设我们选了环上最大边,我们加一条边就能复原这个环,则断开环上的一条边,图仍然联通,显然可以断开那个最大边,得到更小生成树

//我们定义Aj - j × D和Ai + i × D为左边和右边的点的权值

//那么结论是:连接左右的边,至少有一个端点是权值最小的点

//我们设在右子区间中(Ai + i × D)最小点为a,次小点为b,左子区间中(Aj - j × D)最小点为x,次小点为y

//则a<->x<->b<->y<->a构成一个环,则MST一定不包含严格最大边y<->b,符合上述结论

//接下来只需证明有一边相等,即a和b权值相等的情况。

//假设x和y已经联通,那显然不会选y,符合上述结论

//如果我们不通过a和b而是通过更左边的点联通x和y,则情况没有变化。下面考虑通过a和b使x和y联通

//假设a和b未联通,则需从4条边中选三条,显然可以选a<->y、a<->x、b<->x

//假设a和b已联通,则显然可以选a<->y、a<->x

//综上,“连接左右的边,至少有一个端点是权值最小的点”是正确的,我们可以对将区间分治递归连上需要的边,边数变为O(NlogN)

#include <bits/stdc++.h>
#define int long long

using namespace std;

const int N = 2e5 + 5;

const int M = 5e6 + 5;

int n, D, f[N], A[N], wl[N], wr[N];

struct Edge {
    int u, v, w;
        bool operator < (const Edge& x) const {
        return w < x.w;
    }
} e[M];

void init() { for (int i = 1; i <= n; i++) f[i] = i; }
int find(int x) { return x == f[x] ? f[x] : f[x] = find(f[x]); }
int merge(int x, int y) { return find(x) == find(y) ? 0 : f[find(x)] = find(y);}

int cnt = 0;
void add_edge(int l, int r) {
        if (l == r) return;
    int mid = l + r >> 1, minl = l, minr = mid + 1;
    for (int i = l; i <= mid; i++)
        if (wl[i] < wl[minl]) minl = i;
    for (int i = mid + 1; i <= r; i++)
        if (wr[i] < wr[minr]) minr = i;
    for (int i = l; i <= mid; i++)
        e[++cnt] = {i, minr, wl[i] + wr[minr]};
    for (int i = mid + 1; i <= r; i++)
        e[++cnt] = {minl, i, wl[minl] + wr[i]};
    add_edge(l, mid);
    add_edge(mid + 1, r);
}

int kruskal() {
    int res = 0;
    init();
    sort(e + 1, e + 1 + cnt);
    for (int i = 1; i <= cnt; i++) {
        int fu = find(e[i].u), fv = find(e[i].v);
        if (fu == fv) continue;
        f[fu] = fv;
        res += e[i].w;
        if (++cnt == n - 1) break;
    }
    return res;
}

signed main() {
    cin >> n >> D;
    for (int i = 1; i <= n; i++) {
        cin >> A[i];
        wl[i] = A[i] - i * D;
        wr[i] = A[i] + i * D;
    }
    add_edge(1, n);
    cout << kruskal();
    return 0;
}

R-建设道路2 解题报告

题目大意

求以r为根的最小有向生成树(树形图)。

\(n \le 100,m \le 1\times 10^4\)

解题思路

固定根的最小树形图可以用朱-刘算法解决。

首先是预处理,删除自环并判断根节点是否可以到达其它所有节点。如果不是,输出无解并终止程序。
接下来是算法的主过程:
首先,给所有非根节点选择一条权最小的入边。

如果选出来的n-1条边不构成圈,则可以证明这些边就形成了一个最小树形图,否则把每个圈各收缩成一个点,继续上述过程。

缩圈之后,圈上所有边都消失了,因此在最终答案里需要加上这些边权之和。但这样做有个问题:假设在算法的某次迭代中,把圈C收缩为人工节点v,则在下一次迭代中,给v选择的入弧将与在圈C中的入弧发生冲突。(假设X在圈中已经有了入弧Y->X)

因此如果收缩之后又选了一个入弧Z->X,必须把弧Y->X从最小树形图中删除。这等价于把弧Z->X的权值减少了Y->X的权值。

代码实现

//
// Created by vv123 on 2022/4/16.
//
//邻接矩阵实现的朱刘算法
//实现参考:《算法竞赛入门经典——训练指南》
#include <bits/stdc++.h>
using namespace std;
const int N = 110;
const int INF = 0x3f3f3f3f;

struct MinimumDirectedSpanningTree {
    int n, m, s, ans, w[N][N], vis[N], del[N], pre[N], miw[N], cid[N], cycle_cnt;
    MinimumDirectedSpanningTree() {
        memset(w, 0x3f, sizeof w);
        for (int i = 1; i <= n; i++) w[i][i] = 0;
        memset(vis, 0, sizeof vis);
        memset(del, 0, sizeof del);
        memset(pre, 0, sizeof pre);
        memset(miw, 0, sizeof miw);
        memset(cid, 0, sizeof cid);
        ans = 0;
        cycle_cnt = 0;
    }

    int check_vis(int s) {  //检查s能否到达所有结点
        vis[s] = 1;
        int res = 1;
        for (int i = 1; i <= n; i++)
            if(!vis[i] && w[s][i] < INF) res += check_vis(i);
        return res;
    }

    void upd(int u) {   //更新最短入弧miw
        miw[u] = INF;
        for (int i = 1; i <= n; i++)
            if (!del[i] && w[i][u] < miw[u])
                miw[u] = w[i][u], pre[u] = i;   //ans = sum of all miw[x]
    }

    bool find_cycle(int u) {   //判环
        cycle_cnt++;
        int v = u;
        while (cid[v] != cycle_cnt)
            cid[v] = cycle_cnt, v = pre[v]; //沿着pre走一圈
        return v == u;
    }

    void shrink(int u) {    //缩环——我们要删掉环中u以外的所有点v,并用w[i][v]更新w[i][u],用w[v][i]更新w[u][i]
        int v = u;
        do {
            if (v != u) del[v] = 1;
            ans += miw[v];
            for (int i = 1; i <= n; i++) {  //处理环外的点i
                if (cid[i] != cid[u] && !del[i]) {
                    if (w[i][v] < INF) w[i][u] = min(w[i][u], w[i][v] - miw[v]);//v的出边-=miw[v],相当于从环中删掉了这条边
                    if (pre[i] == v) pre[i] = u;
                    w[u][i] = min(w[u][i], w[v][i]);//v的出边直接相连,因为这样并不会改变被计算的边权
                }
            }
            v = pre[v]; //沿着pre走一圈
        } while(v != u);
        upd(u);
    }

    void solve() {
        memset(vis, 0, sizeof vis);
        if (check_vis(s) != n) {
            puts("-1");
            return;
        }

        for (int i = 1; i <= n; i++) upd(i);    //求最短弧集
        pre[s] = s; miw[s] = 0; //注意处理s
        while (1) {
            bool flag = false;  //判断最短弧集中是否存在环
            for (int i = 1; i <= n; i++) {
                if (i != s && !del[i] && find_cycle(i)) {
                    flag = true;
                    shrink(i);  //有环就缩环,更新最短弧集
                    break;
                }
            }
            if (!flag) break;    //当前最短弧集已构成生成树
        }
        for(int i = 1; i <= n; i++)
            if(!del[i]) ans += miw[i];  //ans = sum of all miw[x]
        cout << ans << endl;
    }
};





int main() {
    MinimumDirectedSpanningTree T;
    cin >> T.n >> T.m >> T.s;
    for (int i = 1; i <= T.m; i++) {
        int u, v, w;
        cin >> u >> v >> w;
        T.w[u][v] = min(T.w[u][v], w);
    }
    T.solve();
    return 0;
}

T-我彻底理解了V圈! 解题报告

题目大意

给一张有向图,问从图中最少需要选出多少条互不相交的简单路径(不走重点的一条有向链),才能让图中的所有点都被这些路径经过一次?

\(n \le 200,m \le 6000\)

解题思路

DAG的最小路径覆盖 = 原图上的点数 - 最大匹配数

我们可以感性理解一下:

一开始图中没有边相连,每个点都是一个路径,答案为n。

当我们选择一条边(u,v),则合并了两条以u、v为端点的路径,答案减小1。

路径不能有交点,则图上不能有两条边连接同一个点。

而这正是”匹配“的定义,求出最大匹配即可得到答案。

DAG是一个二分图,因此我们只需要对原图跑一遍匈牙利算法求出最大匹配数cnt,输出n-cnt即可

代码实现

//
// Created by vv123 on 2022/4/22.
//
#include <bits/stdc++.h>
using namespace std;
const int N = 2e6 + 10;
int n, m, match[N], book[N];
vector<int> g[N];

int find(int u) {
    for (auto v:g[u]) {
        if (!book[v]) { //book数组在对每一个男主开启dfs前都会清零,表示在本次尝试中,这名女主是否已经被别人(别人既可能是main里的u也可是递归产生的u),当前的u只能从被book剩下的当中选(这时这名女主没有book,但可能有了match,可以尝试让它之前match的男主另寻所爱,腾给当前的u)
            book[v] = 1;
            if (!match[v] || find(match[v])) {  //如果v单身,或者给v的原配找到了另一个女友,那v的现任就为u
                match[v] = u;
                return true;
            }
        }
    }
    return false;
}



int main() {
    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        int u, v;
        cin >> u >> v;
        g[u].push_back(v);
    }
    int cnt = 0;
    for (int i = 1; i <= n; i++) {
        memset(book, 0, sizeof book);
        if (find(i)) cnt++;
    }
    cout << n - cnt << endl;
    return 0;
}
posted @ 2022-06-19 14:07  _vv123  阅读(75)  评论(0编辑  收藏  举报