加载中...

acwing算法基础课III

acwing 算法基础课III.

DFS

排列数字

注意下用state表示时候的运算顺序.

取第 istate >> i & 1state | (1 << i);

int path[10];
int n;
void dfs(int cnt, int state) {
    if (cnt == n) {
        rep(i, 0, n) O(path[i]);
        puts("");
        return;
    }
    rep(i, 0, n) {
        if ((state >> i & 1) == 0) { // 注意这里一定要括起来才行..
            path[cnt] = i + 1;
            dfs(cnt + 1, state | (1 << i));
        }
    }
}
int main() {
    n = read();
    dfs(0, 0);
    return 0;
}
n-皇后问题

x+yx-y+n 两个表示斜线

int n;
bool dx[30], dy[30], sy[30]; //简单的三个状态表示就行了.
char g[20][20];
void dfs(int x) {
    if (x == n) {
        rep(i, 0, n) {
            rep(j, 0, n) printf("%c", g[i][j]);
            puts("");
        }
        puts("");
        return;
    }
    rep(y, 0, n) {
        if (!dx[x + y] && !dy[y - x + n] && !sy[y]) {
            dx[x + y] = dy[y - x + n] = sy[y] = true; 
            g[x][y] = 'Q';
            dfs(x + 1);
            dx[x + y] = dy[y - x + n] = sy[y] = false;
            g[x][y] = '.';
        }
    }
}
int main() {
    n = read();
    rep(i, 0, n) rep(j, 0, n) g[i][j] = '.';
    dfs(0);
    return 0;
}
树的重心

DFS好处, 遍历过程中, 可以求出来子树的点的个数的.

复杂度是 \(O(n+m)\)的.

int h[N5], e[N5 * 2], ne[N5 * 2], idx, ans = N5, n;
bool st[N5]; // h是邻接表, e和ne是边.

void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;// 加上一条边.
}

int dfs(int u) {
    st[u] = true;
    int size = 0, sum = 0;
    for (int i = h[u];i != -1;i = ne[i]) {
        int j = e[i];
        if (st[j]) continue; // 如果 st 为 true 应该是父节点.
        int s = dfs(j);
        size = max(size, s);
        sum += s;
    }
    size = max(size, n - sum - 1); // 子节点总数 + 当前 + 父节点那块 = n;
    ans = min(ans, size);
    return sum + 1;
}
int main() {
    n = read();
    memset(h, -1, sizeof h); //树初始时候 变成 -1;
    rep(i, 0, n - 1) {
        int a = read(), b = read();
        add(a, b), add(b, a);
    }
    dfs(1);
    O(ans);
    return 0;
}

BFS

走迷宫

雪菜额外维护了一个 d[N2][N2] 来判断起点到每个点的距离.

char g[N2][N2];
int  dx[4] = { 1,-1,0,0 }, dy[4] = { 0,0,-1,1 };

int main() {
    queue<PII> q;
    int n = read(), m = read();
    rep(i, 0, n)rep(j, 0, m) g[i][j] = read();
    q.push({ 0,0 });
    g[0][0] = 1;
    int res = 0;
    while (q.size()) {
        res++;
        int cnt = q.size();
        while (cnt--) {
            auto t = q.front();
            rep(i, 0, 4) {
                int x = t.first + dx[i], y = t.second + dy[i];
                if (x == n - 1 && y == m - 1) {
                    O(res); return 0;
                }
                if (x >= 0 && x < n && y >= 0 && y < m && !g[x][y])
                    q.push({ x,y }), g[x][y] = 1; // ... 等于 写成 == 导致无限循环...
            }
            q.pop();
        }
    }
    return 0;
}
八树码

注意是 unordered_set 不是 set ... 要不然会 TLE;

int dx[] = { 1,-1,0,0 }, dy[] = { 0,0,1,-1 };
unordered_set<string> ss;
string res = "12345678x";
int main() {
    string s;
    char op[3];
    rep(i, 0, 9) { cin >> op; s += op[0]; }
    queue<string> que;
    if (s == res) { O(0);return 0; }
    que.push(s);
    int ans = 1;
    while (!que.empty())
    {
        int cnt = que.size();
        while (cnt--) {
            s = que.front(); que.pop();
            int p = s.find('x');
            rep(i, 0, 4) {
                int x = p / 3 + dx[i], y = p % 3 + dy[i];
                if (x >= 0 && x < 3 && y >= 0 && y < 3) {
                    swap(s[x * 3 + y], s[p]);
                    if (!ss.count(s)) {
                        ss.insert(s), que.push(s);
                        if (s == res) { O(ans);return 0; }
                    }
                    swap(s[x * 3 + y], s[p]);
                }
            }
        }
        ans++;
    }
    O(-1);
    return 0;
}
图中点的层次
int n, m, e[N5], h[N5], ne[N5], idx, st[N5];
void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;// 加上一条边.
}

int main() {
    n = read(), m = read();
    memset(h, -1, sizeof h); //树初始时候 变成 -1;
    rep(i, 0, m) {
        int a = read(), b = read();
        add(a, b);
    }
    queue<int> qu;
    qu.push(1);
    int res = 0;
    while (!qu.empty()) {
        int cnt = qu.size();
        while (cnt--) {
            int t = qu.front();
            if (t == n) { O(res);return 0; }
            for (int i = h[t];i != -1;i = ne[i]) {
                int j = e[i]; // 记住这样才能把边取出来
                if (!st[j]) {
                    st[j] = true;
                    qu.push(j);
                }
            }
            qu.pop();
        }
        res++;
    }
    O(-1);
    return 0;
}

拓扑排序

有向图拓扑排序

使用自定义的队列, 可以存下来 拓扑排序的顺序, 妙啊;

const int N = N5;
int n, m, h[N], e[N], ne[N], q[N], d[N], idx;

void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
bool bfs() {
    int hh = 0, tt = -1;
    rep(i, 1, n + 1) {
        if (!d[i]) q[++tt] = i;
    }
    while (hh <= tt) {
        int t = q[hh++];
        // cout << t << endl;
        for (int i = h[t]; i != -1;i = ne[i]) {
            int j = e[i];
            // cout << "   " << j << endl;
            if (--d[j] == 0) q[++tt] = j;
        }
    }
    return hh == n; // 注意这里 hh是n 而 tt是 n-1;
}
int main() {
    n = read(), m = read();
    memset(h, -1, sizeof h);
    rep(i, 0, m) {
        int a = read(), b = read();
        add(a, b);
        d[b]++;
    }
    if (bfs()) { rep(i, 0, n)O(q[i]); }
    else { O(-1); }
    return 0;
}

最短路

img
单源最短路.

  1. 所有边权都是正的. 朴素 Dijkstra \(O(n^2)\) 和 堆优化的Dijkstra算法 \(O(mlogn)\).
  2. 存在负权边. Bellman-Ford \(O(nm)\) SPFA 一般\(O(m)\) 最坏\(O(nm)\)
    1. 如果限制经过不超过 \(k\) 条边的话, 只能用 bellman-ford 算法

多源汇最短路: 多个起点和多个起点的最短路. 都是不确定的.

  1. Floyd 算法 \(O(n^3)\)

问题: 建图..

dijkstra 最短路

遍历过的边 S 和其他的边 T之间, 每次从T最小 dis 的 放到 S中, 然后更新T中其他边的距离, a,b,w 有 dis[a] + w < dis[b] 就更新一下.

边权一定得是正数.

  1. dist[1] = 0, dist[v] = +inf //其他初始化成正无穷
  2. for v: 0 ~ n \(O(n)\)
    1. 不在 S 中的距离最近的点\(\to\) t \(O(n)\)
    2. t \(\to\) S
    3. t 更新其他点的距离: t 的所有的边, dist[x] > dist[t] + w 更新一下. \(O(n)\)
const int N = 500 + 10;
int dist[N];
int g[N][N];
bool st[N];
int n, m, t;
int dijkstra() {
    dist[1] = 0;
    rep(j, 0, n - 1) { // 只需要 n - 1次, 因为最后只剩一个点了.
        t = -1;
        rep(i, 1, n + 1)
            if (!st[i] && (t == -1 || dist[i] < dist[t])) t = i;
        if(t == n) break;
        st[t] = true;
        rep(i, 1, n + 1)
            dist[i] = min(dist[i], dist[t] + g[t][i]);
        // On(dist[n]);
    }
    return dist[n] == 0x3f3f3f3f ? -1 : dist[n];
}
int main() {
    n = read(), m = read();
    memset(g, 0x3f, sizeof g);
    memset(dist, 0x3f, sizeof dist);

    while (m--) {
        int a = read(), b = read(), w = read();
        g[a][b] = min(g[a][b], w);
    }
    O(dijkstra());
    return 0;
}
dijkstra 最短路2:

用堆来维护接下来需要遍历的边.

const int N = 1.5e5 + 10;
int n, m, ne[N], w[N], h[N], e[N], dist[N], idx, x;
bool st[N];
void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
int diskstra() {
    pqs(PII) heap;
    dist[1] = 0;
    heap.push({ 0,1 });
    while (!heap.empty()) {
        auto top = heap.top();
        heap.pop();
        int distance = top.first, x = top.second; // haha 这里写反了...
        if (st[x]) continue;
        st[x] = true;
        for (int i = h[x];i != -1;i = ne[i]) {
            int j = e[i];
            if (dist[x] + w[i] < dist[j]) {
                dist[j] = dist[x] + w[i],
                    heap.push({ dist[j],j });
            }
        }
    }
    return dist[n] == 0x3f3f3f3f ? -1 : dist[n];
}
int main() {
    memset(h, -1, sizeof h);
    memset(dist, 0x3f, sizeof dist);
    n = read(), m = read();
    while (m--) {
        int a = read(), b = read(), c = read();
        add(a, b, c);
    }
    O(diskstra());
    return 0;
}
Bellman-ford 最短路 ↪️
  1. for 1 : n

    1. for 所有边 a,b,w. 开结构体. 随便存
    2. dist[b] = min(dist[b], dist[a] + w) 松弛操作.

    循环完成以后有 dist[b] <= dist[a] + w

多少条边 遍历多少次. 直接用结构体存就可以.

然后可以判断负环, 如果存在, n次遍历以后还在更新, 那么就说明有一条大于n长度的最短路径, 也就是说有负环.

O(nm)的算法.

最多k条边的最短路径, 只能用bellman-ford

每次备份一下 当前的dis数组, 才能保证在 k 的限制之内. memcpy

struct {
    int a, b, w;
}edges[N4];
int n, m, k, dist[N4], backup[N4];
int main() {
    n = read(), m = read(), k = read();
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    rep(i, 0, m) {
        int a = read(), b = read(), w = read();
        edges[i] = { a,b,w };
    }
    rep(i, 0, k) {
        memcpy(backup, dist, sizeof dist);
        rep(i, 0, m) {
            int a = edges[i].a, b = edges[i].b, w = edges[i].w;
            dist[b] = min(dist[b], backup[a] + w);
            // 这里错了.... 要怎么改呢? 备份一下dist ?
        }
    }
    if (dist[n] >= 0x3f3f3f3f / 2)   puts("impossible");
    else O(dist[n]);
}
spfa

dis[b] = min(dis[b], dis[a]+w) 如果 dis[a] 不变的话, dis[b] 也不会变.

BFS进行, 如果变小, 就加入队列, 反复迭代.

  1. node1 \(\to\) queue
  2. while(!queue.empty())
    1. t <- q.front.
    2. 更新 t 的所有出边. b <- t . queue <- b; //这里 t 代表着 dist[a] 变小了.
int n, m, h[N5], w[N5], ne[N5], e[N5], idx, dist[N5];
bool st[N5];

void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
void spfa() {
    queue<int> que;
    que.push(1), st[1] = true;
    // O(n);
    while (!que.empty()) {
        int a = que.front(); que.pop(); st[a] = false;
        for (int i = h[a]; i != -1; i = ne[i]) {
            int b = e[i], c = w[i];
            if (dist[b] > dist[a] + c) {
                dist[b] = dist[a] + c;
                if (!st[b]) {
                    // 这里一定要加上判断 st[b] .. yes 不加的话. st就没用了啊...
                    que.push(b);
                    st[b] = true;
                }
            }
        }
        da(dist, n + 1);
    }
}
int main() {
    n = read(), m = read();
    memset(h, -1, sizeof h);
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    rep(i, 0, m) {
        int a = read(), b = read(), c = read();
        add(a, b, c);
    }
    spfa();
    if (dist[n] >= 0x3f3f3f3f / 2)   puts("impossible");
    else O(dist[n]);
}
spfa求负环

路径上一定存在一个环.

初始时候每个边都放到queue中, 然后求得时候用cnt[N]如果大于了 n 就说明有负环了.

  1. 进行更新 dist[b] = dist[a] + w 的时候就 cnt[b] = cnt[a] + 1 说明多了一条边
  2. 如果某一次 cnt[b] >= n 了 那就说明已经有负环了, 抽屉原理.
int n, m, h[N5], w[N5], ne[N5], e[N5], idx, dist[N5], cnt[N5];
bool st[N5];

void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
bool spfa() {
    queue<int> que;
    rep(i, 1, n + 1) que.push(i), st[i] = true; //I. 这里好像要每个点都push进去. 
    // O(n);
    while (!que.empty()) {
        int a = que.front(); que.pop(); st[a] = false;
        for (int i = h[a]; i != -1; i = ne[i]) {
            int b = e[i], c = w[i];
            if (dist[b] > dist[a] + c) {
                dist[b] = dist[a] + c;
                cnt[b] = cnt[a] + 1; //II. 是这样更新的.....
                if (cnt[b] >= n) return true;
                if (!st[b]) {
                    que.push(b);
                    st[b] = true;
                }
            }
        }
        da(dist, n + 1);
    }
    return false;
}

int main() {
    n = read(), m = read();
    memset(h, -1, sizeof h);
    rep(i, 0, m) {
        int a = read(), b = read(), c = read();
        add(a, b, c);
    }
    puts(spfa() ? "Yes" : "No");
}
Floyd最短路

多源汇最短路问题. 输出很多..

d[k, i, j] 表示只经过前k 个点, 从 ij 的最短距离.

d[k,i,j] (min=) d[k-1, i, k] + d[k-1, k, j] 加上第 k 个点, 会不会变短.

动态规划.

不能有负权回路, 负无穷的最短路径.

const int N = 210, M = N4 * 2, INF = 0x3f3f3f3f;
int n, m, k, x, d[N][N]; // I. 这里的 g 和 d 可以共用一个东西.
int main() {
    n = read(), m = read(), k = read();
    rep(i, 1, n + 1)
        rep(j, 1, n + 1)
        if (i == j) d[i][i] = 0; // II. 这里的初始化可以memset(inf)?? 没错.
        else d[i][j] = INF;
    rep(i, 0, m) {
        int a = read(), b = read(), c = read();
        d[a][b] = min(d[a][b], c);
    }
    rep(k, 1, n + 1) { // III 遍历所有 k 条边
        rep(i, 1, n + 1)
            rep(j, 1, n + 1) // IV 遍历所有 i -> j 的路径
            d[i][j] = min(d[i][j], d[i][k] + d[k][j]); // V 可以变成 i -> j -> k  吗?
    }
    rep(i, 0, k) {
        int a = read(), b = read();
        if (d[a][b] > INF / 2) puts("impossible");
        else On(d[a][b]);
    }
}

生成树.

img

prim 最小生成树

一般都是无向图 (可以处理负权的树.)

任意两个城市之间铺公路的最小长度.

  1. 所有 dist[i] 初始化成正无穷
  2. rep(i,0,n)
    1. t \(\leftarrow\) 集合外距离最近的点. (第一步随便挑)
    2. 用t来更新其他点到集合的距离 // 只有和disktra不同. 有没有一条边能连向集合内部.
    3. st[t] = true 加入到集合中.
      1. 只有集合这里和dijkstra不同.
const int N = 510, M = N4 * 2, INF = 0x3f3f3f3f;
int n, m, k, x, d[N][N], dist[N];
bool st[N];
int main() {
    n = read(), m = read();
    memset(dist, 0x3f, sizeof dist);
    rep(i, 1, n + 1)
        rep(j, 1, n + 1)
        if (i == j) d[i][i] = 0;
        else d[i][j] = INF;
    rep(i, 0, m) {
        int a = read(), b = read(), c = read();
        d[a][b] = d[b][a] = min(d[a][b], c);
    }
    int ans = 0;
    dist[1] = 0; // I 这里切记不敢让 st[1] = true 了.
    rep(j, 0, n) { // O(n)
        int t = -1;
        rep(i, 1, n + 1) if (!st[i] && (t == -1 || dist[i] < dist[t])) t = i; // O(n)
        st[t] = true;
        // II. 找到 dist 最小的点. 并 更新状态.
        if (dist[t] >= INF / 2) { ans = INF;break; }
        // III. 如果不连通, false ;
        ans += dist[t]; // IV: 当前距离加入到生成树中 需要在 V. 前面才能处理自环.
        rep(i, 1, n + 1) dist[i] = min(dist[i], d[t][i]);
        // V: 更新集合外其他点的距离. O(n)
    }
    if (ans >= INF / 2) puts("impossible");
    else O(ans);
} // O(n^2)

//todo 堆优化. 基本可以被 kruskal 取代掉, 所以不用了.

kruskal
  1. 将所有边按照权重从大到小排序.
    1. O(m logm) 常数非常小
  2. 枚举每条边, a,b,w 如果a, b 不连通, 将w加到集合中.
    1. 并查集. O(m)
const int M = 2 * N5;
int p[N5];
vector<pair<int, PII>> e;

struct Edge{
    int a, b, w;
    bool operator< (const Edge &W)const // I. 如何重载 比较符. 
    {
        return w < W.w;
    }
}  
int find(int x) {
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}
int main() {
    int n = read(), m = read();
    rep(i, 0, m) {
        int a = read(), b = read(), c = read();
        e.push_back({ c,{a,b} });
    }
    sort(e.begin(), e.end()); // O(mlogm) I. 所有边需要进行排序.
    rep(i, 1, n + 1) p[i] = i;
    int ans = 0, cnt = 0;
    for (auto i : e) { // O(m) 并查集好像是 O(logm) 的? 不过有剪枝.
        int w = i.first, a = i.second.first, b = i.second.second;
        if (find(a) != find(b)) {
            ans += w; // II. 从小到大遍历, 把最小的边加进来.
            cnt++;
            p[find(a)] = find(b);
        }
    }
    if (cnt != n - 1) puts("impossible");
    else O(ans);
    return 0;
}

二分图

一个图是二分图, 当且仅当图中不含奇数环.

染色法.

判断是否是二分图.

给一个图进行 dfs 染色, 如果 染色出现矛盾以后, 就不是一个二分图了.

int n, m, c[N5], e[N5 * 2], h[N5], ne[N5 * 2], idx;
// I. 注意 边的个数要是原来的 2 倍才行
// II. st 可以直接用 c 代替就行...
bool ok = true;
void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void dfs(int a, int w) {
    if (!ok) return;
    c[a] = w;
    for (int i = h[a]; i != -1;i = ne[i]) {
        int j = e[i];
        if (c[j]) { if (c[j] != 3 - w) ok = false; } 
        // I. 染色的时候用 3 - w 判断二分图的两边.
        else dfs(j, 3 - w);
    }
}
int main() {
    n = read(), m = read();
    memset(h, -1, sizeof h);
    rep(i, 0, m) {
        int a = read(), b = read();
        add(a, b), add(b, a); // 无向图.
    }
    rep(i, 1, n + 1) {
        if (!c[i]) dfs(i, 1); // 每个点都需要染色.
    }
    puts(ok ? "Yes" : "No");
    return 0;
}
匈牙利算法 二分图匹配

返回图中匹配成功的数量最大值.

如果想要去匹配的人已经有人了, 那就尝试换一个.

O(n * m) 实际时间 远小于 O(n * m)

这个回溯怎么写的还是没懂..

// 好像是个递归吗?
const int N = 500, M = 1e5;
int n1, n2, m, e[M], h[N], ne[M], idx;
bool st[N];
int match[N]; // I. 需要一个 match 数组存放当前的匹配.
void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
bool find(int x) {
    for (int i = h[x]; i != -1; i = ne[i]) { // O(m);
        int j = e[i]; 
        if (!st[j]) { // 如果还没有遍历过. 遍历过了, 也能跳过, 防止递归那里出问题.
            st[j] = true; // 这个用 st 是很好的东西, 是否遍历过
            if (!match[j] || find(match[j])) { //  如果还没有, 或者 能找到新的, 这里是个递归.
                match[j] = x;
                return true;
            }
        }
    }
    return false;
}
int main() {
    n1 = read(), n2 = read(), m = read();
    memset(h, -1, sizeof h);
    rep(i, 0, m) {
        int a = read(), b = read();
        add(a, b);
    }
    int ans = 0;
    rep(i, 1, n1 + 1) { // O(n)
        memset(st, 0, sizeof st); // 开始前每一次需要全部初始化成 0;
        if (find(i)) ans++;
    }
    O(ans);
    return 0;
}









posted @ 2022-02-11 12:51  benenzhu  阅读(91)  评论(0)    收藏  举报