省选小复习 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]);
}
}