【算法竞赛】ACM/ICPC 常见模板分享

100 阅读14分钟

算法竞赛常见模板

分层图最短路

板子是:m条边,可以任选 k 条路免费走,求最短路。

和普通的最短路区别不大,但是要想到分层建图。

例子:

小薇要开始一场星际旅行!小薇有一艘飞船,要驾驶这艘飞船穿越一个星系。

这个星系可以简化为一个N个点,M条有向边的地图,其中小薇会确定一个起点S和终点T。一共存在三种星球,3类星球可以任意航行不受限制,而小薇已经知道,要想顺利到达终点,飞船必须经过正好一颗1类星球和一颗2类星球,并且1类星球需要先于2类星球经过,否则,小薇将会到达一个邪恶的地方,这对小薇的身体不好。

已知每穿过一条航线i,会消耗飞船 wi的能量,请你帮助小薇计算出在正确从起点到达终点的前提下,消耗的最小能量是多少。

参考:

struct node {
    int ennd, wi;
};
int d[N], u[N];
vector<node> v[N];
bool ve[N];
void dij() {
    priority_queue<par, vector<par>, greater<par>> q;
    q.push({0, stt});
    memset(d, 0x3f, sizeof d);
    d[stt] = 0;
    while (!q.empty()) {
        int x = q.top().second;
        q.pop();
        if (ve[x]) continue;
        ve[x] = 1;
        for (auto [ed, z] : v[x]) {
            if (d[ed] > d[x] + z) {
                d[ed] = d[x] + z;
                q.push({d[ed], ed});
            }
        }
    }
}
void add(int a, int b, int c) {
    v[a].push_back({b, c});
}
signed main() {
    n = read();
    m = read();
    stt = read();
    enn = read();
    rep(i, 1, n) u[i] = read();
    rep(i, 1, m) {
        a = read();
        b = read();
        t = read();
        // 建图
        if (u[a] == 3) {
            add(a, b, t);
            add(a + n, b + n, t);
            add(a + 2 * n, b + 2 * n, t);
        } else if (u[a] == 2) {
            add(a + n, b + 2 * n, t);
        } else if (u[a] == 1) {
            add(a, b + n, t);
        }
    }
    dij();
    if (d[enn + 2 * n] == INF)
        cout << -1;
    else
        cout << d[enn + 2 * n];
    return 0;
}

分层图最长路

约翰有 n 块草场,编号 1 到 n,这些草场由若干条单行道相连。奶牛贝西是美味牧草的鉴赏家,她想到达尽可能多的草场去品尝牧草。

贝西总是从 1 号草场出发,最后回到 1 号草场。她想经过尽可能多的草场,贝西在通一个草场只吃一次草,所以一个草场可以经过多次。因为草场是单行道连接,这给贝西的品鉴工作带来了很大的不便,贝西想偷偷逆向行走一次,但最多只能有一次逆行。问,贝西最多能吃到多少个草场的牧草。

题目链接:www.luogu.com.cn/problem/P31…

这题需要缩点

void tarjan(int x) {
    dfn[x] = low[x] = ++num;
    stk[++top] = x;
    ins[x] = 1;
    for (auto y : v[x]) {
        if (!dfn[y]) {
            tarjan(y);
            low[x] = min(low[x], low[y]);
        } else if (ins[y])
            low[x] = min(low[x], dfn[y]);
    }
    if (dfn[x] == low[x]) {
        sccnum++;
        int z;
        do {
            z = stk[top--];
            ins[z] = 0;
            c[z] = sccnum;
            wi[sccnum]++;
        } while (x != z);
    }
}
void dij() {
    queue<int> q;
    d[stt] = 0;
    q.push(stt);
    while (!q.empty()) {
        int x = q.front();
        // cout << x << "\n";
        q.pop();
        ve[x] = 0;
        for (auto y : vc[x]) {
            int z = wi[y];
            if (d[y] < d[x] + z) {
                d[y] = d[x] + z;
                if (!ve[y]) q.push(y), ve[y] = 1;
            }
        }
    }
}
signed main() {
    n = read();
    m = read();
    rep(i, 1, m) {
        a = read();
        b = read();
        add(a, b);
    }
    rep(i, 1, n) if (!dfn[i]) tarjan(i);
    rep(x, 1, n) {
        for (auto y : v[x]) {
            if (c[y] == c[x]) continue;
            addc(c[x], c[y]);
            addc(c[y], c[x] + sccnum);
            addc(c[x] + sccnum, c[y] + sccnum);
        }
    }
    rep(i, 1, sccnum) wi[i + sccnum] = wi[i];
    stt = c[1];
    dij();
    cout << max(d[stt], d[c[1] + sccnum]);
    return 0;
}
​

DFS序

树上多个点的LCA,就是DFS序最小的和DFS序最大的这两个点的LCA。

这可以用来判断,一个点是否在另外一个点的子树里面

int indfn[N], outdfn[N];
// tot=0;
void dfs(int x, int fa) {//求dfn序
    indfn[x] = ++tot;
    for (auto y : v[x]) {
        if (y == fa) continue;
        dfs(y, x);
    }
    outdfn[x] = tot;
}
bool isin(int u, int v) {//判断
    return indfn[u] <= indfn[v] && outdfn[v] <= outdfn[u];
}
bool is_link(vector<int> p) {
    sort(p.begin(), p.end(), cmp);
    for (int i = 1; i < p.size(); i++) {
        if (!isin(p[i - 1], p[i])) return 0;
    }
    return 1;
}

最近公共祖先:LCA(nlogn)

void bfs() {
    queue<int> q;
    q.push(1);
    dpth[1] = 1;
    while (!q.empty()) {
        int x = q.front();
        q.pop();
        for (auto y : v[x]) {
            if (dpth[y]) continue;
            dpth[y] = dpth[x] + 1;
//此处可以顺便求从原点出发的距离/异或路径等
            q.push(y);
            f[y][0] = x;
            rep(j, 1, t) {
//此处可以通过dp求别的东西 用类似的思想
                f[y][j] = f[f[y][j - 1]][j - 1];
            }
        }
    }
}
int lca(int x, int y) {
    if (dpth[x] > dpth[y]) swap(x, y);
    per(i, t, 0) {
        if (dpth[x] <= dpth[y] - (1 << i)) y = f[y][i];
    }
    if (x == y) return x;
    per(i, t, 0) {
        if (f[x][i] != f[y][i]) x = f[x][i], y = f[y][i];
    }
    return f[x][0];
}
​
    t = log(n) / log(2) + 1;
    bfs();
​
//将树上一个点往上移动depth长度所到达的点
int lca(int u, int depth) {
    for (int i = 20; i >= 0; i--) {
        if (depth <= dpth[u] - (1 << i)) { u = f[u][i]; }
    }
    return u;
}

轻/重链剖分

  • 重儿子:对于每一个非叶子节点,它的儿子中 以那个儿子为根的子树节点数最大的儿子 为该节点的重儿子。
  • 轻儿子:对于每一个非叶子节点,它的儿子中 非重儿子 的剩下所有儿子即为轻儿子。
  • 重边:一个父亲连接他的重儿子的边称为重边。
  • 轻边:剩下的即为轻边。
  • 重链:相邻重边连起来的 连接一条重儿子 的链叫重链。每一条重链以轻儿子为起点。

对于叶子节点,若其为轻儿子,则有一条以自己为起点的长度为1的链。

int fa[N];    //父节点
int hson[N];  //重儿子
int dpth[N];  //深度
int siz[N];   //子树大小
vector<int> v[N];
// dpth[root]=1
void dfs1(int x, int f) {
    //处理以上四个信息
    fa[x] = f;
    dpth[x] = dpth[f] + 1;
    siz[x] = 1;
    int now = -1;  //重儿子的儿子个数
    for (auto y : v[x]) {
        if (y == f) continue;
        dfs1(y, x);
        siz[x] += siz[y];
        if (siz[y] > now) {
            now = siz[y];
            hson[x] = y;
        }
    }
}
​
​
int u[N];
int id[N];    //新编号
int wid[N];   //新编号的权值
int ntop[N];  //点所在的链的顶端
//处理(确定)新编号
//处理链的顶端
void dfs2(int x, int tp) {
    id[x] = ++tot;
    wid[tot] = u[x];
    ntop[x] = tp;
    if (!hson[x]) return;  //无子节点 也就是x是叶子节点
    dfs2(hson[x], tp);     //先处理重儿子
    for (auto y : v[x]) {
        if (y == hson[x] || y == fa[x]) continue;
        dfs2(y, y);  //重链的起点都是儿子
    }
}
​
// 1.查询一条链上的区间和
int askL(int a, int b) {
    int ans = 0;
    while (ntop[a] != ntop[b]) {
        if (dpth[ntop[a]] < dpth[ntop[b]]) swap(a, b);  //始终让a链的顶端更深
        ans += query(1, id[ntop[a]], id[a]);            //线段树上的区间查询
        ans %= mod;
        a = fa[ntop[a]];
    }
    if (dpth[a] < dpth[b]) swap(a, b);  //让a更深
    ans += query(1, id[b], id[a]);      //因为一条链上新编号连续 且上边的新编号更小 所以id[a]不会小于id[b]
    ans %= mod;
    return ans;
}
​
// 2.区间修改一条链上的值
void changeL(int a, int b, int z) {
    while (ntop[a] != ntop[b]) {
        if (dpth[ntop[a]] < dpth[ntop[b]]) swap(a, b);  //始终让a链的顶端更深
        change(1, id[ntop[a]], id[a], z);               //线段树上的区间查询
        a = fa[ntop[a]];
    }
    if (dpth[a] < dpth[b]) swap(a, b);  //让a更深
    change(1, id[b], id[a], z);         //因为一条链上新编号连续 且上边的新编号更小 所以id[a]不会小于id[b]
    return;
}
​
//查询和修改大致相同 区别只在于调用线段树的操作// 3.查询子树和
int askT(int x) {
    return query(1, id[x], id[x] + siz[x] - 1);  //每一个子树的新编号也是连续的,所以可以直接求
}
​
// 4.子树修改
void changeT(int x, int z) {
    change(1, id[x], id[x] + siz[x] - 1, z);
}
​

启发式合并

启发式合并就是在两个集合合并的时候,将size小的那个集合合并到size大的那个集合里面。

NlogN的复杂度,因为每个元素至多被pushback logn次

void merge(vector<int>&a, vector<int>&b) {
    if (a.size() > b.size()) {
        for (int x : b)a.push_back(x);
    }
    else {
        for (int x : a)b.push_back(x);
    }
}

线段树求和

struct sgt {
    int l, r, sum, add;
} tree[4 * N];
​
void build(int p, int l, int r) {
    tree[p].l = l;
    tree[p].r = r;
    if (l == r) {
        tree[p].sum = wid[l];
        tree[p].sum %= mod;
        return;
    }
    int mid = (l + r) >> 1;
    build(2 * p, l, mid);
    build(2 * p + 1, mid + 1, r);
    tree[p].sum = (tree[2 * p].sum + tree[2 * p + 1].sum) % mod;
}
void spread(int p) {
    if (tree[p].add != 0) {
        tree[2 * p].add += tree[p].add;
        tree[2 * p + 1].add += tree[p].add;
        tree[2 * p].sum += tree[p].add * (tree[2 * p].r - tree[2 * p].l + 1);
        tree[2 * p + 1].sum += tree[p].add * (tree[2 * p + 1].r - tree[2 * p + 1].l + 1);
        tree[2 * p].sum %= mod;
        tree[2 * p + 1].sum %= mod;
        tree[2 * p].add %= mod;
        tree[2 * p + 1].add %= mod;
        tree[p].add = 0;
    }
}
​
void change(int p, int l, int r, int d) {
    if (l <= tree[p].l && r >= tree[p].r) {
        tree[p].sum += d * (tree[p].r - tree[p].l + 1);
        tree[p].add += d;
        tree[p].sum %= mod;
        tree[p].add %= mod;
        return;
    }
    spread(p);
    int mid = (tree[p].l + tree[p].r) >> 1;
    if (l <= mid) change(2 * p, l, r, d);
    if (r > mid) change(2 * p + 1, l, r, d);
    tree[p].sum = tree[2 * p].sum + tree[2 * p + 1].sum;
    tree[p].sum %= mod;
    return;
}
​
int query(int p, int l, int r) {
    if (l <= tree[p].l && r >= tree[p].r) return tree[p].sum;
    spread(p);
    int mid = (tree[p].l + tree[p].r) >> 1;
    int val = 0;
    if (l <= mid) val += query(2 * p, l, r);
    if (r > mid) val += query(2 * p + 1, l, r);
    val %= mod;
    return val;
}
​

线段树区间赋值

struct sgt {
    int l, r, sum, add;
} tree[4 * N];
​
void build(int p, int l, int r) {
    tree[p].l = l;
    tree[p].r = r;
    tree[p].add = -1;
    if (l == r) return;
    int mid = (l + r) >> 1;
    build((p << 1), l, mid);
    build((p << 1) | 1, mid + 1, r);
    tree[p].sum = (tree[(p << 1)].sum + tree[(p << 1) | 1].sum);
}
void spread(int p) {
    if (tree[p].add != -1) {
        tree[(p << 1)].add = tree[p].add;
        tree[(p << 1) | 1].add = tree[p].add;
        tree[(p << 1)].sum = tree[p].add * (tree[(p << 1)].r - tree[(p << 1)].l + 1);
        tree[(p << 1) | 1].sum = tree[p].add * (tree[(p << 1) | 1].r - tree[(p << 1) | 1].l + 1);
        tree[p].add = -1;
    }
}
​
void change(int p, int l, int r, int d) {
    if (l <= tree[p].l && r >= tree[p].r) {
        tree[p].sum = d * (tree[p].r - tree[p].l + 1);
        tree[p].add = d;
        return;
    }
    spread(p);
    int mid = (tree[p].l + tree[p].r) >> 1;
    if (l <= mid) change((p << 1), l, r, d);
    if (r > mid) change((p << 1) | 1, l, r, d);
    tree[p].sum = tree[(p << 1)].sum + tree[(p << 1) | 1].sum;
    return;
}
​
int query(int p, int l, int r) {
    if (l <= tree[p].l && r >= tree[p].r) return tree[p].sum;
    spread(p);
    int mid = (tree[p].l + tree[p].r) >> 1;
    int val = 0;
    if (l <= mid) val += query((p << 1), l, r);
    if (r > mid) val += query((p << 1) | 1, l, r);
    return val;
}
​

李超树

//李超线段树
//添加一条线段(给出起点终点)
//询问x=k处 所有线段的max
//每个节点存的是该区间优势最大的线段
//依次枚举包含 x=k 的线段树区间,取每个区间的最大优势线段的对应纵坐标的最大值即可
//对于垂直于x的线段 存储为y=0x+y(l=r=x) (y是上顶点)  因为只关心最大值 同时要能够表达出这条线段

struct line {
    dd k, b;
} u[N];
int tree[4 * N];
dd Calc(line a, int x) {
    return a.k * x + a.b;
}
int query(int p, int l, int r, int x) {  // 单点查询
    if (l == r) return tree[p];
    int mid = (l + r) >> 1;
    if (x <= mid) {
        int tmp = query(p << 1, l, mid, x);
        return Calc(u[tmp], x) - Calc(u[tree[p]], x) > EPS ? tmp : tree[p];
    } else {
        int tmp = query(p << 1 | 1, mid + 1, r, x);
        return Calc(u[tmp], x) - Calc(u[tree[p]], x) > EPS ? tmp : tree[p];
    }
}
// Modify(1, 1, N, aaa, ccc, cnt);
// 1, 左区间 ,右区间,修改的区域l,r, 和线段的k,b
inline void Modify(int p, int l, int r, int s, int t, int x) {  // 插入一条线段
    int mid = (l + r) >> 1;
    if (s <= l && r <= t) {  //完全包含
        if (Calc(u[x], mid) - Calc(u[tree[p]], mid) > EPS) swap(tree[p], x);
        if (Calc(u[x], l) - Calc(u[tree[p]], l) > EPS) Modify(p << 1, l, mid, s, t, x);
        if (Calc(u[x], r) - Calc(u[tree[p]], r) > EPS) Modify(p << 1 | 1, mid + 1, r, s, t, x);
        return;
    }
    if (s <= mid) Modify(p << 1, l, mid, s, t, x);
    if (t > mid) Modify(p << 1 | 1, mid + 1, r, s, t, x);
}

signed main() {
    cf {
        Q = read();
        if (Q == 1) {
            cin >> a >> b >> c >> d;
            aaa = (a + mas - 1) % 39989 + 1;
            bbb = (b + mas - 1) % 1000000000 + 1;
            ccc = (c + mas - 1) % 39989 + 1;
            ddd = (d + mas - 1) % 1000000000 + 1;
            if (aaa > ccc) swap(aaa, ccc), swap(bbb, ddd);
            if (aaa == ccc)
                u[++cnt] = {0, (dd)max(bbb, ddd)};
            else
                u[++cnt] = {1.0 * (ddd - bbb) / (ccc - aaa), bbb - 1.0 * (ddd - bbb) / (ccc - aaa) * aaa};
            Modify(1, 1, N, aaa, ccc, cnt);
        } else {
            k = read();
            kkk = (k + mas - 1) % 39989 + 1;
            // ask()
            mas = query(1, 1, N, kkk);
            cout << mas << "\n";
        }
    }
    return 0;
}

主席树

应用:求静态区间第k小的数

// 利用了前缀和 线段树维护的是权值的分布数量
// 作差 即可得到区间范围的值的个数
int u[N], vv[N];
int rot[N];
struct node {
    int l, r, val;
} tree[N << 5];
int clone(int p) {
    tot++;
    tree[tot] = tree[p];
    return tot;
}
int build(int p, int l, int r) {
    p = ++tot;
    if (l == r) return p;
    int mid = (l + r) >> 1;
    tree[p].l = build(tree[p].l, l, mid);
    tree[p].r = build(tree[p].r, mid + 1, r);
    return p;
}
int add(int p, int l, int r, int val) {
    p = clone(p);
    tree[p].val++;
    if (l == r) { return p; }
    int mid = (l + r) >> 1;
    if (val <= mid) {
        tree[p].l = add(tree[p].l, l, mid, val);
    } else {
        tree[p].r = add(tree[p].r, mid + 1, r, val);
    }
    // tree[p].val = tree[tree[p].l].val + tree[tree[p].r].val;
    return p;
}
int ask(int p1, int p2, int l, int r, int x) {
    if (l == r) { return l; }
    int mid = (l + r) >> 1;
    int y = tree[tree[p2].l].val - tree[tree[p1].l].val;  //左子的个数
    if (x <= y) {
        return ask(tree[p1].l, tree[p2].l, l, mid, x);
    } else {
        return ask(tree[p1].r, tree[p2].r, mid + 1, r, x - y);
    }
}

signed main() {
    n = read();
    m = read();
    rep(i, 1, n) u[i] = read(), vv[i] = u[i];
    sort(vv + 1, vv + 1 + n);
    int x = unique(vv + 1, vv + 1 + n) - vv - 1;
    rot[0] = build(0, 1, x);
    rep(i, 1, n) {
        int now = lower_bound(vv + 1, vv + 1 + x, u[i]) - vv;
        rot[i] = add(rot[i - 1], 1, x, now);
    }
    rep(i, 1, m) {
        l = read();
        r = read();
        k = read();
        cout << vv[ask(rot[l - 1], rot[r], 1, x, k)] << "\n";
    }
    return 0;
}

可持久化数组

用主席树来实现。操作:单点修改 单点查询

注意数组要开大

struct kkk {
    int l, r, val;
} tree[25 * N];//m*logn+4*N
int u[N];
int rot[N];  //每个版本号所对应的树根的编号

int clone(int p) {
    top++;
    tree[top] = tree[p];  //全部信息都传到新节点
    return top;
}

int build(int p, int l, int r) {
    //与普通线段树的区别:节点编号不再是2*p+1 而是用结构体存储左右子节点
    //单点修改 每次只复制一份被修改的数据 所以每次空间只多了logn 动态开点 省空间
    //另外用数组存每个历史版本的树根
    //修改树边 只要修改子节点即可
    //返回的是 树的根的编号
    p = ++top;
    if (l == r) {
        tree[p].val = u[l];
        return p;
    }
    int mid = (l + r) >> 1;
    tree[p].l = build(tree[p].l, l, mid);
    tree[p].r = build(tree[p].r, mid + 1, r);
    return p;
}
int update(int p, int l, int r, int x, int val) {  // p是版本号所对应的根
    p = clone(p);                                  //更新就要新建根节点(new版本)
    if (l == r) {
        tree[p].val = val;
    } else {
        int mid = (l + r) >> 1;
        if (x <= mid)
            tree[p].l = update(tree[p].l, l, mid, x, val);  //访问左子树
        else
            tree[p].r = update(tree[p].r, mid + 1, r, x, val);  //访问右子树
    }
    return p;
}
int ask(int p, int l, int r, int x) {
    if (l == r) {
        return tree[p].val;
    } else {
        int mid = (l + r) >> 1;
        if (x <= mid)
            return ask(tree[p].l, l, mid, x);  //访问左子树
        else
            return ask(tree[p].r, mid + 1, r, x);  //访问右子树
    }
}

ST求最值

//t=log(n)/log2)+1;
void st() {
    rep(i, 1, n) {
        rep(j, 1, 32) {
            g[i][j] = 0;
        }
    }
    rep(i, 1, n) g[i][0] = as[i];
    rep(j, 1, t) {
        rep(i, 1, n - (1 << j) + 1) {
            g[i][j] = max(g[i][j - 1], g[i + (1 << (j - 1))][j - 1]);
        }
    }
}
int query(int l, int r) {
    if (l > r) return 0;
    int k = log(r - l + 1) / log(2);
    return max(g[l][k], g[r - (1 << k) + 1][k]);
}

KRUSKAL重构树

int find(int x) {
    if (x == bcj[x]) return x;
    return bcj[x] = find(bcj[x]);
}
struct node {
    int u, v, w;
} u[N];
bool cmp(node a, node b) {
    return a.w < b.w;
}
void kruskal() {
    // sort(u + 1, u + 1 + m, cmp);
    int lim = (n << 1) - 1;
    rep(i, 1, lim) v[i].clear();
    tot = n;
    // cout << tot << "\n";
    rep(i, 1, lim) bcj[i] = i;
    rep(i, 1, m) {
        int x = find(u[i].u);
        int y = find(u[i].v);
        if (x == y) continue;
        tot++;
        bcj[x] = tot;
        bcj[y] = tot;
        bcj[tot] = tot;
        val[tot] = u[i].w;
        v[tot].push_back(x);
        v[tot].push_back(y);
        if (tot == lim) {
            break;
        }
    }
}

Spfa判正环

vector<par> v[N];
bool is[N];
dd d[N];
int ct[N];
bool spfa() {
    queue<int> q;
    rep(i, 1, n) d[i] = 0, ct[i] = 0, is[i] = 1, q.push(i);
    while (!q.empty()) {
        int x = q.front();
        q.pop();
        is[x] = 0;
        for (auto [y, w] : v[x]) {
            w = log(w);//边权太大 取对数
            if (d[y] < d[x] + w) {
                d[y] = d[x] + w;
                ct[y] = ct[x] + 1;
                if (ct[y] >= n) return 0;//有
                if (!is[y]) q.push(y), is[y] = 1;
            }
        }
    }
    return 1;//无
}

求二元二次函数最值

三分化为一元,然后公式求出极值点,On判三分,需要注意精度问题,三分终止条件可以写成次数而非r-l的差值

dd ask(dd fir) {
    return 1.0 * sum / n - (n - 1) / 2.0 * fir;
}
dd f(dd fir) {
    dd k = ask(fir);
    dd now = k;
    dd ans = 0;
    ans += (u[1] - now) * (u[1] - now);
    rep(i, 2, n) {
        now += fir;
        ans += (u[i] - now) * (u[i] - now);
    }
    return ans;
}
signed main() {
    cf {
        n = read();
        sum = 0;
        rep(i, 1, n) u[i] = read(), sum += u[i];
        dd l = -1e9, r = 1e9;
        cnt = 0;
        while (cnt++ <= 120) {
            dd m1 = l + (r - l) / 3;
            dd m2 = r - (r - l) / 3;
            if (f(m2) > f(m1)) {
                r = m2;
            } else
                l = m1;
        }
        printf("%.9f\n", (double)f(l));
    }
    return 0;
}