省选小复习 2024

省选小复习 2024

主要以代码能力为主,个人向(临时抱佛脚)。
本文同步(可能有延迟)发布于:cnblogs, luogu blog.

二分

应用场景:单调性/二段性问题求解,部分最优化问题转换为判定性问题,三分,整体二分。
转换方法是:求 XXX 最小值 -> 答案值域为 \([L,R]\),判定当答案小于等于 \(mid\) 时是否有解,设最小的解为 \(x\),则当 \(x\) 小于等于 \(mid\) 时一定有解,大于时一定无解,具有二段性,可以二分。求出的最小的 \(mid\) 就是 \(x\)

例题:P7514,AGC006D

注意一些二分可以解决的问题双指针也可以解决,具体问题具体分析。

常规二分模板:曾经写的博客

三分可以求单峰函数极值点,注意如果一个函数是凸函数或者凹函数,则一定是单峰函数,反之不一定成立。

整体二分就是把同一类可以二分的多个问题放在一起二分判定求解,注意过程中的数据结构优化,代码框架如下:


void solve(int lval, int rval, int ql, int qr) // 当前二分的范围为 [lval,rval],询问在数组中编号 [ql,qr]
{
    if (ql > qr)
        return;
    if (lval == rval)
    {
        for (int i = ql; i <= qr; i++)
            // 求得这部分答案为 lval
        return;
    }
    int mid = (lval + rval) >> 1;
    int ltot = 0, rtot = 0;
    for (int i = ql; i <= qr; i++)
    {
        // 对整体进行二分划分
        if (q[i].y <= mid)
            lq[++ltot] = q[i];
        else
            rq[++rtot] = q[i];
    }
    for (int i = 1; i <= ltot; i++)
        q[ql + i - 1] = lq[i];
    for (int i = 1; i <= rtot; i++)
        q[ql + ltot + i - 1] = rq[i];
    // 分治求解
    solve(lval, mid, ql, ql + ltot - 1);
    solve(mid + 1, rval, ql + ltot, qr);
}

二叉堆、哈夫曼树

优先队列的写法:

priority_queue<int, vector<int>, less<int>> q1; // 大根堆,重载 < 运算符
priority_queue<int, vector<int>, greater<int>> q1; // 小根堆,重载 > 运算符

二叉堆能解决的常见问题:ABC254Ex

数位 DP

好像暑假回来就没碰过这个东西了,求解的问题是某一个区间满足某个性质的数的个数。实现方法有组合计数和记忆化搜索。只会记忆化搜索,代码模板:

string l, r;
int a[N];
int dp[N][N];
int len;
int dfs(int pos, int r, bool limit)
{
    // 当前填 pos 位
    if (pos == 0)
        return /*初始状态*/;
    if (!limit && (dp[pos][r] != -1))
        return dp[pos][r]; // 无限制条件下的记忆化搜索
    int ans = 0;
    int up = limit ? a[pos] : 9;
        for (int i = 0; i <= up; i++)
            if (/*分析条件*/)
                ans = (ans + dfs(pos - 1, /*转移*/, limit && i == a[pos])) % mod;
    if (!limit)
        dp[pos][r] = ans;
    return ans;
}
int solve(string &s)
{
    memset(dp, -1, sizeof dp);
    len = s.length();
    for (int i = 0; i < len; i++)
        a[i + 1] = s[len - i - 1] - '0'; // 把数字 s 按位填
    return dfs(len, 0, 1);
}

例题:CF628D,

鸽巢原理

原理内容小学生都知道(应该),考试要想得到。尤其是类似于存在性问题、构造等。
CF618F

启发式合并

每次把大的集合合并到小的啥的,树上数颜色可以考虑重链剖分什么的。
可以考虑线段树合并(个人感觉更好写)。

CF600E,P3224

倍增

参考 LCA 的倍增求法,可以记录 DP 起点为 \(2^i\) 啥的来构造 dp[][i] = dp[dp[][i-1]][i-1] 这样的转移。
P1081

可持久化线段树

没啥好说的,考前看眼代码咋写。
(话说这个菜鸡到现在一种树套树都没学)

struct node
{
    int lc, rc;
    int val;
} t[N * 80];
int tot, root[N];
int push_up(int p)
{
    return t[p].val = t[t[p].lc].val + t[t[p].rc].val, p;
}
int insert(int p, int id, int cl, int cr)
{
    int q = ++tot;
    t[q] = t[p]; // 先复制
    if (cl == cr)
        return t[q].val++, q;
    int mid = (cl + cr) >> 1;
    if (id <= mid)
        t[q].lc = insert(t[p].lc, id, cl, mid);
    else
        t[q].rc = insert(t[p].rc, id, mid + 1, cr);
    return push_up(q);
}
int query(int p, int q, int k, int cl, int cr)
{
    if (cl == cr)
        return cl;
    int mid = (cl + cr) >> 1, lv = t[t[q].lc].val - t[t[p].lc].val;
    if (k <= lv)
        return query(t[p].lc, t[q].lc, k, cl, mid);
    return query(t[p].rc, t[q].rc, k - lv, mid + 1, cr);
}

字符串哈希

老朋友了。考场上可以想一些随机权值哈希(不可以,总司令)。【数据结构】Hash 学习笔记

状态压缩 DP

先预处理出可以互相转移的状态。
子集枚举技巧:for (int ov = (ou - 1) & ou; ov; ov = (ov - 1) & ou)

CDQ 分治

解决离线问题的好助手。三维偏序:第一维排序,第二维分治,第三维数据结构。

void cdq(int l, int r)
{
    if (l >= r)
        return;
    int mid = (l + r) / 2;
    cdq(l, mid), cdq(mid + 1, r);
    int i = l, j = mid + 1, k = l - 1;
    while (i <= mid && j <= r)
    {
        if (da[i].b <= da[j].b) // 按第二维归并排序
            BIT::add(da[i].c, da[i].cnt),
                db[++k] = da[i++];
        else
            da[j].res += BIT::query(da[j].c),
                db[++k] = da[j++];
    }
    while (i <= mid)
        BIT::add(da[i].c, da[i].cnt),
            db[++k] = da[i++];
    while (j <= r)
        da[j].res += BIT::query(da[j].c),
            db[++k] = da[j++];
    for (i = l; i <= mid; i++)
        BIT::add(da[i].c, -da[i].cnt);
    for (i = l; i <= r; i++)
        da[i] = db[i];
}

线段树合并

int merge(int p, int q, int cl, int cr)
{
    if (!p || !q)
        return p | q;
    if (cl == cr)
    {
        // 合并叶子结点
        return p;
    }
    int mid = (cl + cr) / 2;
    t[p].lc = merge(t[p].lc, t[q].lc, cl, mid);
    t[p].rc = merge(t[p].rc, t[q].rc, mid + 1, cr);
    return push_up(p);
}

矩阵相关

矩阵乘法具有结合率,根本原因是乘法对加法具有分配率。\((a+b) * c = a * c + b * c\)
类似的,\(min(a,b) + c\) = \(min(a+c,b+c)\),所以也可以用这两种运算定义矩阵乘法。

矩阵快速幂加速 DP 方程转移。

网络流

背背 Dinic,这个蒟蒻没学建模。
最大流最小割定理、平面图最小割转对偶图最短路(代码不会写)。

bool bfs()
{
    memset(d, 0, sizeof d);
    queue<int> q;
    q.push(S), d[S] = 1, now[S] = head[S];
    while (!q.empty())
    {
        int u = q.front();
        q.pop();
        for (int e = head[u]; e; e = nxt[e])
        {
            int v = to[e], c = lc[e];
            if (d[v] || !c)
                continue;
            d[v] = d[u] + 1;
            now[v] = head[v];
            q.push(v);
            if (v == T)
                return true;
        }
    }
    return false;
}
int dinic(int u, int flow)
{
    if (u == T)
        return flow;
    int rest = flow;
    for (int &e = now[u]; e; e = nxt[e])
    {
        int v = to[e], c = lc[e];
        if (d[v] != d[u] + 1 || !c)
            continue;
        int k = dinic(v, min(c, rest));
        if (!k)
            d[v] = 0;
        lc[e] -= k, lc[e ^ 1] += k, rest -= k;
        if (!rest)
            break;
    }
    return flow - rest;
}

图的连通性

Tarjan 算法的三种变形,记忆 low 的求法和连通分量的判定方法。

求有向图强连通分量。在同一个强连通分量中,任意两个点可达对方。
有向图通过强连通分量缩完点后是一个 DAG。

void tarjan(int u) {
    dfn[u] = low[u] = ++tot;
    stk[++top] = u, in_stk[u] = true;
    for (int e = head[u]; e; e = nxt[e]) {
        int v = to[e];
        // low 的求法
        if (!dfn[v]) {
            tarjan(v);
            low[u] = min(low[u], low[v]);
        } else if (in_stk[v]) {
            low[u] = min(low[u], dfn[v]);
        }
    }
    if (dfn[u] == low[u]) { // 连通分量的判定方法
        int x = ++scc_cnt;
        int y;
        do {
            y = stk[top--];
            scc[y] = x;
            in_stk[y] = false;
            scc_size[x]++;
        } while (y != u);
    }
}

求无向图边双连通分量。一个边双连通分量删去任意一条边仍然是连通块。
无向图通过边双连通分量缩完点后是一棵树。

void tarjan(int u, int from)
{
    dfn[u] = low[u] = ++tim;
    stk[++top] = u;
    for (int e = head[u]; e; e = nxt[e])
    {
        // 求 low 值
        int v = to[e];
        if (!dfn[v])
        {
            tarjan(v, e);
            low[u] = min(low[u], low[v]); 
            if (dfn[u] < low[v]) // 桥的判断
                is_bridge[e] = is_bridge[e ^ 1] = true;
        }
        else if (e != (from ^ 1))
            low[u] = min(low[u], dfn[v]);
    }
    if (dfn[u] == low[u]) // 判断边双
    {
        int x = ++dcc_cnt, v = stk[top];
        do
        {
            v = stk[top--];
            dcc[x].push_back(v);
        } while (u != v);
    }
}

求无向图点双连通分量。一个点双连通分量删去任意一个点仍然是连通块。
无向图通过点双连通分量缩完点后是圆方树(这个蒟蒻没学)。

void tarjan(int u, int fa)
{
    dfn[u] = low[u] = ++tim;
    stk[++top] = u;
    for (int e = head[u]; e; e = nxt[e])
    {
        // 求 low 值
        int v = to[e];
        if (v == fa)
            continue;
        if (!dfn[v])
        {
            tarjan(v, u);
            low[u] = min(low[u], low[v]);
            if (dfn[u] <= low[v]) // 一个点双
            {
                vdcc[++vdcc_cnt].push_back(u);
                ++deg[u]; // deg[u] > 1 说明 u 是割点
                int vv = stk[top];
                do
                {
                    vv = stk[top--];
                    vdcc[vdcc_cnt].push_back(vv);
                    ++deg[vv];
                } while (vv != v);
            }
        }
        else
            low[u] = min(low[u], dfn[v]);
    }
}
posted @ 2024-02-29 12:51  蒟蒻OIer-zaochen  阅读(16)  评论(0编辑  收藏  举报