图论速写

比数据结构简单一些
字符串?逆天
如果我复杂度分析有问题或者在说什么屁话的话,请在评论区薄纱我谢谢

A

别问,问就是时空战士(指用暴力算法和现代CF机子草过远古CF题)
说正经的
两个参数\(s,g\),要求最小值,那么我们肯定就要固定一个值,然后让另一个最小
所以直接对边按照\(s\)排序,然后顺序将边加入进来
对于每个\(s\)作为一个断点跑一次\(Kruscal\)求一次最小值
如果直接这么暴力会T,复杂度是\(O(m^2logm)\)
\(n \le 200,m \le 50000\),可以观察到\(n << m\),于是想让复杂度和\(n\)相似
可以发现对于维护的边里面存在单调性,即如果在这次生成树中我用不到那么之后加进来新的边我也还是用不到
所以维护边集大小为\(n\)即可,时间复杂度变成\(O(n^2logn)\)
然后就草过去了,正解应该是\(O(nm)\)然后还可以再优化

A
int n, m, res; ll G, S;
struct edge { int u, v, g, s; }e[M], l[M];
inline bool cmp1 (const edge &A, const edge &B) { return A.g < B.g; }
inline bool cmp2 (const edge &A, const edge &B) { return A.s < B.s; }
int f[M]; ll ans = inf;
inline int find(int x) { return f[x] == x ? x : f[x] = find(f[x]); }
inline void un(int x, int y) { f[find(y)] = find(x); }
void kruscal() {
    rep(i, 1, n) f[i] = i;
    sort(l + 1, l + 1 + res, cmp2);
    int mg = 0, ms = 0, cnt = 0;
    rep(i, 1, res) {
        int u = l[i].u, v = l[i].v;
        if (find(u) == find(v)) {
            res--;
            rep(j, i, res) l[j] = l[j + 1];
            i--;
            continue;
        }
        un(u, v), cnt++;
        ms = Max(ms, l[i].s), mg = Max(mg, l[i].g);
        if (cnt == n - 1) { res = i; break; }
    }
    if (cnt < n - 1) return;
    ans = Min(ans, (ll)mg * G + (ll)ms * S);
}

WWW main() {
    n = read(), m = read(), G = read(), S = read();
    rep(i, 1, m) e[i].u = read(), e[i].v = read(), e[i].g = read(), e[i].s = read();
    sort(e + 1, e + 1 + m, cmp1);
    rep(i, 1, m) {
        l[++res] = e[i];
        if (i < n - 1) continue;
        kruscal();
    }
    print(ans == inf ? -1 : ans, 0);
    return 0;
}

B

求严格次小生成树,那当然要先跑一个最小生成树出来
考虑\(Kruscal\)算法的正确性,那么只要断掉一条边,再连上一条边权更大的且不改变联通性的剩下的没有被加入的边,取个\(min\)即可求出
那么没有被加入的边在\(Kruscal\)时就可以处理出来,考虑加这条边的话,我能删去的就是这条边连接两个点到其最小生成树的\(lca\)的路径上的边
所以树剖维护树上边权最大值,因为要求严格小于,所以还需要维护次大值
然后这题就没了
要特判掉自环的情况

B
struct Gragh{ 
    struct edge { int v, w, next; }e[M << 1];
    int head[M], tot = 1;
    void add(int u, int v, int w) { e[tot].v = v, e[tot].w = w, e[tot].next = head[u], head[u] = tot++; } 
}E;
struct DSU {
    int f[M];
    inline void init(int n) { rep(i, 1, n) f[i] = i; }
    inline int find(int x) { return f[x] == x ? x : f[x] = find(f[x]); }
    inline void un(int x, int y) { f[find(y)] = find(x); }
}F;
int n, m, res; ll tot;
struct edge { 
    int u, v, w; 
    friend bool operator< (const edge &A, const edge &B) { return A.w < B.w; }
}g[M * 3], q[M * 3];
void kruscal() {
    sort(g + 1, g + 1 + m); F.init(n); int cnt = n - 1;
    rep(i, 1, m) {
        int u = g[i].u, v = g[i].v;
        if (F.find(u) == F.find(v)) {
            q[++res] = g[i];
            continue;
        }
        F.un(u, v), E.add(u, v, g[i].w), E.add(v, u, g[i].w), tot += g[i].w;
        cnt--;
        if (!cnt) { rep(j, i + 1, m) q[++res] = g[j]; break; }
    }
}
int fa[M], siz[M], dep[M], top[M], son[M], dfn[M], w[M], rnk[M], tim;
void dfs1(int x, int fat) {
    fa[x] = fat, siz[x] = 1, dep[x] = dep[fa[x]] + 1;
    edg(i, x) {
        int v = E.e[i].v;
        if (v == fa[x]) continue;
        w[v] = E.e[i].w;
        dfs1(v, x);
        siz[x] += siz[v];
        if (siz[v] > siz[son[x]]) son[x] = v;
    }
}
void dfs2(int x, int pa) {
    top[x] = pa, dfn[x] = ++tim, rnk[tim] = x;
    if (son[x]) dfs2(son[x], pa);
    edg(i, x) {
        int v = E.e[i].v;
        if (v == fa[x] or v == son[x]) continue;
        dfs2(v, v);
    }
}
struct tree { int max1, max2; tree() { max1 = max2 = 0; } }e[M << 2];
#define ls (rt << 1)
#define rs ((rt << 1) | 1)
inline tree pushup(tree l, tree r) {
    tree rt;
    if (l.max1 == r.max1) {
        rt.max1 = l.max1;
        rt.max2 = Max(l.max2, r.max2);
    }else {
        rt.max1 = Max(l.max1, r.max1);
        if (rt.max1 == l.max1) rt.max2 = Max(l.max2, r.max1);
        else rt.max2 = Max(l.max1, r.max2);
    }
    return rt;
}
void build(int rt, int l, int r) {
    if (l == r) { e[rt].max1 = w[rnk[l]]; return; }
    int mid = l + r >> 1;
    build(ls, l, mid), build(rs, mid + 1, r);
    e[rt] = pushup(e[ls], e[rs]);
}
tree query(int rt, int l, int r, int L, int R) {
    if (L > R) { tree k; return k; }
    if (L <= l and r <= R) return e[rt];
    int mid = l + r >> 1;
    if (L > mid) return query(rs, mid + 1, r, L, R);
    if (R <= mid) return query(ls, l, mid, L, R);
    return pushup(query(ls, l, mid, L, R), query(rs, mid + 1, r, L, R));
}
tree query(int u, int v) {
    tree ans;
    while(top[u] != top[v]) {
        if (dep[top[u]] < dep[top[v]]) swap(u, v);
        ans = pushup(ans, query(1, 1, n, dfn[top[u]], dfn[u]));
        u = fa[top[u]];
    }
    if (dep[u] > dep[v]) swap(u, v);
    return pushup(ans, query(1, 1, n, dfn[u] + 1, dfn[v]));
    return ans;
}
WWW main() {
    n = read(), m = read();
    rep(i, 1, m) g[i].u = read(), g[i].v = read(), g[i].w = read();
    kruscal();
    dfs1(1, 0), dfs2(1, 1); build(1, 1, n);
    ll tmp = inf;
    rep(i, 1, res) {
        if (q[i].u == q[i].v) continue;
        tree k = query(q[i].u, q[i].v);
        if (q[i].w > k.max1) tmp = Min(tmp, q[i].w - k.max1);
        else tmp = Min(tmp, q[i].w - k.max2);
    }
    print(tot + tmp, 0);
    return 0;
}

C

感觉挺简单的
\(dij\)每次能确定一个点,那么最后一次更新这个点的边肯定是我想要的
那么我就可以贪心选,每次进去一个点我就保留最后一条边,那么每一条边都能保留一个"好点"
然后最后跑出来保留下来的就是一棵树,就是最短路径树
然后边\(dij\)边贪心这个题就做完了
因为只需要\(n-1\)条边就可以保留所有点,所以\(k=min(k,n-1)\)
了解了一下最短路径树,在这里放两道例题,有兴趣可以看一看(都挺水的
感觉其实这个东西主要还是\(dij\)贪心的思想
Paths and Trees
Berland and the Shortest Paths

C
struct edge{ int id, v, w, next; }e[M << 1];
int head[M], tot = 1;
void add(int u, int v, int w, int id) { e[tot].id = id, e[tot].v = v, e[tot].w = w, e[tot].next = head[u], head[u] = tot++; }
int n, m, k;
ll dis[M]; bool vis[M];
struct node {
    int dis, point, from;
    node() {} node(int a, int b, int c) { dis = a, point = b, from = c; }
    friend bool operator< (const node &A, const node &B) { return A.dis > B.dis; } 
};
priority_queue<node>q;
void dij() {
    memset(dis, 1, sizeof(dis));
    dis[1] = 0;
    q.push(node(0, 1, 0));
    while(!q.empty()) {
        node l = q.top(); q.pop();
        int u = l.point;
        if (vis[u]) continue;
        vis[u] = 1;
        if (l.from) {//特判0
            print(l.from, 0);
            k--;
            if (!k) return;
        }
        for (int i = head[u]; i; i = e[i].next) {
            int v = e[i].v;
            if (dis[v] > dis[u] + e[i].w) {
                dis[v] = dis[u] + e[i].w;
                q.push(node(dis[v], v, e[i].id));
            }
        }
    }
}

WWW main() {
    n = read(), m = read(), k = read(); if (!k) return print(0, 0), 0;
    rep(i, 1, m) { int u = read(), v = read(), w = read(); add(u, v, w, i), add(v, u, w, i); }
    k = Min(k, n - 1); print(k, 1);
    dij();
    return 0;
}

D

一眼感觉不是什么可做的题,那估计不是搜就是D
好了,是记搜或者D
我们先找出\((s,t)\)的最短路,设其长度为\(k\),那么我们要求出\((s,t)\)距离为\(k/k+1\)的路径方案数,
那么设\(f_{x,0/1}\)为从\(s\)\(x\)的距离为\((s,x)\)最短路的距离或距离\(+1\)的方案数
转移的话直接就搜
\(f_{v,1} += f_{x,0}, [dis_v = dis_x]\)
\(f_{v,0} += f_{x,0}, f_{v,1} += f_{x,1}, [dis_v = dis_x + 1]\)
上面是多走一条边产生贡献,下面是正常贡献
硬搜因为会反复搜到重复状态,T到飞起,那么记搜就已经可以过掉了(我觉得,且我没写
我们考虑优化转移,每次都是向\(dis\)相同或\(+1\)的状态转移,所以进行桶排转移即可,复杂度是\(O(n)\)
\(bfs\)求最短路即可,时间复杂度\(O(n)\)

D
struct edge { int v, next; }e[M << 1];
int head[M], tot = 1;
void add(int u, int v) { e[tot].v = v, e[tot].next = head[u], head[u] = tot++; }
int n, m, s, t, dis[M], mdis; ll f[M][2];
vector<int> v[M];
void solve() {
    rep(i, 1, n) v[dis[i]].push_back(i);
    f[s][0] = 1;
    rep(i, 0, mdis) {
        for (auto x : v[i]) {
            for (int i = head[x]; i; i = e[i].next) {
                int v = e[i].v;
                if (dis[v] == dis[x]) (f[v][1] += f[x][0]) %= mod;
            }
        }
        for (auto x : v[i]) {
            for (int i = head[x]; i; i = e[i].next) {
                int v = e[i].v;
                if (dis[v] == dis[x] + 1) (f[v][0] += f[x][0]) %= mod, (f[v][1] += f[x][1]) %= mod;
            }
        }
    }
}
void bfs() {
    queue<int> q;
    memset(dis, 50, sizeof(dis));
    q.push(s);
    dis[s] = 0;    
    while(!q.empty()) {
        int u = q.front(); q.pop();
        mdis = Max(mdis, dis[u]);
        for (int i = head[u]; i; i = e[i].next) {
            int v = e[i].v;
            if (dis[v] > dis[u] + 1) dis[v] = dis[u] + 1, q.push(v);
        }
    }
}

WWW main() {
    int T = read();
    while(T--) {
        n = read(), m = read(), s = read(), t = read();
        rep(i, 1, m) { int u = read(), v = read(); add(u, v), add(v, u); }
        bfs(); solve();
        print((f[t][0] + f[t][1]) % mod, 1);
        if (T) {
            rep(i, 0, mdis) vector<int>().swap(v[i]);
            mdis = 0;
            rep(i, 1, n) f[i][0] = f[i][1] = head[i] = dis[i] = 0;
            tot = 1;
        }
    }
    return 0;
}

E

一开始读错题意了,经典sb操作
考虑答案一定只有一条边通过不断修改贡献而来,不会经过第二条边
感性理解一下:通过不断用这条边走,到了一条更小的边,那么后面我用这条更小的边去走更优,前面的部分也可以用这条更小的边替代,那么依旧是只用一条边即可
问题转化成了两个端点分别走到\(1\)\(n\)次数之和\(+1\)再乘上边权即是答案
后面这个部分枚举每条边即可,前面这个部分先跑一个\(floyed\),然后考虑求这两个点分别到达\(1\)\(n\)的次数之和的最小值
利用\(floyed\)的思想,只有两种情况:

1.我两个点直接走到\(1,n\),即\(min(dis_{1,u} + dis_{v,n}, dis_{1,v} + dis_{u,n})\)

2.两个点先到达中转点\(k\),再分别走到\(1,n\)

中转点是\(1/n\)时特判一下
后面这个情况需要两个点先取一个到\(k\)的最小距离即让其中一个点先到达\(k\),然后再用\(1\)步的贡献让另一个点也到达\(k\)
所以是\(dis_{1_k}+dis_{k,n}+1+min(dis_{u,k}, dis_{v,k})\)
时间复杂度\(O(n^3+nm)\)

E
int n, m, dis[M][M];
WWW main() {
    int T = read();
    while(T--) {
        n = read(), m = read();
        rep(i, 1, n) rep(j, 1, n) dis[i][j] = inf;
        rep(i, 1, m) { int u = read(), v = read(), w = read(); dis[u][v] = dis[v][u] = 1; e[i].u = u, e[i].v = v, e[i].w = w; }
        rep(i, 1, n) dis[i][i] = 0;
        rep(k, 1, n) rep(i, 1, n) rep(j, 1, n) dis[i][j] = Min(dis[i][j], dis[i][k] + dis[k][j]);
        ll ans = 1e18;
        rep(i, 1, m) {
            int x = inf, u = e[i].u, v = e[i].v, w = e[i].w;
            x = Min(x, Min(dis[1][u] + dis[v][n], dis[1][v] + dis[u][n]));
            x = Min(x, dis[1][n] + 1 + Min(Min(dis[1][u], dis[1][v]), Min(dis[u][n], dis[v][n])));
            rep(k, 2, n - 1) x = Min(x, dis[1][k] + 1 + dis[k][n] + Min(dis[u][k], dis[v][k]));
            ans = Min(ans, (ll)(x + 1) * w);
        }
        print(ans, 1);
    }
    return 0;
}

F

你先别急

G

似乎是最简单的,但是为什么感觉是第一次打欧拉回路
两个字符连边,然后求一个字典序最小的欧拉回路即可
判断无解要在联通的基础上有两个点度为奇数或者全为偶数

G
int n, jz[M][M], d[M], f[M], res, st; char c[M];
inline int find(int x) { return f[x] == x ? x : f[x] = find(f[x]); }
inline void un(int x, int y) { f[find(y)] = find(x); }
char ans[M * M];
void dfs(int x) {
    rep(i, 1, 150) if (jz[x][i]) {
        jz[x][i] = jz[i][x] = 0;
        dfs(i);
    }
    ans[n--] = x;
}
WWW main() {
    rep(i, 1, 150) f[i] = i;
    n = read();
    rep(i, 1, n) {
        scanf("%s", c + 1);
        jz[c[1]][c[2]] = jz[c[2]][c[1]] = 1;
        un(c[1], c[2]);
        d[c[1]]++, d[c[2]]++;
    }
    rep(i, 1, 150) if (i == find(i) and d[i]) res++;
    if (res != 1) { puts("No Solution"); return 0; }//非联通图
    res = 0; 
    rep(i, 1, 150) if (d[i] & 1) {
        res++;
        if (!st) st = i;
    }    
    if (!st) rep(i, 1, 150) if (d[i]) { st = i; break; } //选一个最小的起点
    if (res and res != 2) { puts("No Solution"); return 0; }//如果存在度为奇数的点且不是两个,则无解
    dfs(st);
    puts(ans);
    return 0;
}

H

I

别急别急

J

有向图中只要有环,我就可以直接把这个环解决掉,权值即是环内最小值
那这一个\(tarjan\)就解决了
求方案数直接乘法原理,把每个环内最小值个数乘起来即可

J
struct edge { int v, next; }e[M << 1];
int head[M], tot = 1;
void add(int u, int v) { e[tot].v = v, e[tot].next = head[u], head[u] = tot++; }
int n, m, a[M];
int dfn[M], low[M], tim, cnt, stk[M], top; bool vis[M]; ll ans, res = 1;
void dfs(int x) {
    dfn[x] = low[x] = ++tim, stk[++top] = x, vis[x] = 1;
    for (int i = head[x]; i; i = e[i].next) {
        int v = e[i].v;
        if (!dfn[v]) dfs(v), low[x] = Min(low[x], low[v]);
        else if (vis[v]) low[x] = Min(low[x], dfn[v]);
    }
    if (low[x] == dfn[x]) {
        cnt++; int y = 0; ll k = mod, rs = 0;
        do { 
            y = stk[top--], vis[y] = 0;
            if (a[y] < k) k = a[y], rs = 1;
            else if (a[y] == k) rs++;
        }while(x != y);
        ans += k, (res *= rs) %= mod;
    }
}
WWW main() {
    n = read();
    rep(i, 1, n) a[i] = read();
    m = read();
    rep(i, 1, m) { int u = read(), v = read(); add(u, v); }
    rep(i, 1, n) if (!dfn[i]) dfs(i);
    print(ans, 0), print(res, 0);
    return 0;
}

K

因为定义数组叫size,我远古代码交到洛谷直接CE了.....
这道题蓝书上也有,因为当时贺的就是蓝书
先看清楚是有序对
首先能产生贡献的只有割点,那么如果不是割点则只有所有点和这个点不再联通,答案直接就是\(2 \times (n-1)\)
对于割点来讲,有序对可以分成两部分:被割开的儿子,剩下的祖先
处理出\(sum\)为所有能割开的儿子子树大小之和,那么其他的点和这些点都不再连通,再加上自己,贡献是\((n-sum-1) \times (sum + 1) + n - 1\),这一部分是有序对\((\text{剩下的祖先},\text{被割开的儿子})\)
那么每个割开的儿子子树和所有其他的点都不再连通,贡献是\(siz_v * (n - siz_v)\),那么这一部分是有序对\((\text{被割开的儿子},\text{剩下的祖先})\)
没了,没重新打,所以码风有些,emmm,独具一格

K
ll n, m, nn;

struct edge {
	int u, v, next;
}e[M * 10];
int head[M], tot = 1;
void add(int u, int v) {
	e[tot].u = u;
	e[tot].v = v;
	e[tot].next = head[u];
	head[u] = tot++;
}

int siz[M];
int dfn[M], low[M], tim;
ll ans[M];
bool cut[M];
void tarjan(int x) {
	dfn[x] = low[x] = ++tim;
	siz[x] = 1;
	int son = 0;
	ll sum = 0;
	for (int i = head[x]; i; i = e[i].next) {
		int v = e[i].v;
		if (!dfn[v]) {
			son++;
			tarjan(v);
			siz[x] += siz[v];
			low[x] = min(low[x], low[v]);
			if (dfn[x] <= low[v]) {
				sum += siz[v];
				ans[x] += siz[v] * (n - siz[v]);
				if (x != 1|| son > 1) cut[x] = 1;
			}
		}else {
			low[x] = min(low[x], dfn[v]);
		}
	}
	if (!cut[x]) ans[x] = 2 * nn;
	else ans[x] += (n - sum - 1) * (sum + 1) + nn;
}

int main() {
	n = read(); m = read();nn = n - 1;
	for (int i = 1; i <= m; i++) {
		int u = read(), v = read();
		add(u, v);
		add(v, u);
	}
	tarjan(1);
	for (int i = 1; i <= n; i++) printf("%lld\n", ans[i]);
	return 0;
}
posted @ 2022-11-14 17:47  紫飨  阅读(19)  评论(0编辑  收藏  举报