自用:常见算法竞赛/刷题问题 & 模板

以下是我平常刷题遇到的部分常见问题,随手记录一下。(不定时更新)

基本算法

二分与最大值最小化

注意,这里的有序是广义的有序,如果一个数组中的左侧或者右侧都满足某一种条件,而另一侧都不满足这种条件,也可以看作是一种有序(如果把满足条件看做1 ,不满足看做 0,至少对于这个条件的这一维度是有序的)。换言之,二分搜索法可以用来查找满足某种条件的最大(最小)的值,也即“至少满足某条件的最小值 (最大化)”和“最多满足某条件的最大值(最小化)”

以下区间均假设为闭区间。

int binary_search_maximize_min(int L, int R) {
    while (L < R) {
        int mid = (L + R + 1) / 2;  // 注意这里是 (L + R + 1) / 2
        if (check(mid)) {
            L = mid;  // mid 满足条件,尝试更大的值
        } else {
            R = mid - 1;  // mid 不满足条件,尝试更小的值
        }
    }
    return L;  // 或 R,此时 L == R,返回任意一个都可以
}

int binary_search_minimize_max(int L, int R) {
    while (L < R) {
        int mid = (L + R) / 2;  // 注意这里是 (L + R) / 2
        if (check(mid)) {
            R = mid;  // mid 满足条件,尝试更小的值
        } else {
            L = mid + 1;  // mid 不满足条件,尝试更大的值
        }
    }
    return L;  // 或 R,此时 L == R,返回任意一个都可以
}

单调队列 与 滑动窗口

例见:
P1886 滑动窗口 /【模板】单调队列
P1725 琪露诺

单调队列用于维护区间 \([L, R]\) 之间的 \(Max/Min\) 值,通常存放的是索引而非值本身。

	deque<int> dq;
	// 维护 [i - R, i + L] 区间
	for (int i = l; i <= n; ++i)
	{
		int idxToAddToMonoQueue = /* ... */ i - l;
		while (!dq.empty() && f[dq.back()] <= f[idxToAddToMonoQueue]) dq.pop_back();
		dq.push_back(idxToAddToMonoQueue);
		while (!dq.empty() && dq.front() + r < i) dq.pop_front();
		f[i] = f[dq.front()] + a[i];
		if (i + r > n) ans = max(ans, f[i]);
	}

单调栈

见例:
P5788 【模板】单调栈
P1901 发射站

单调栈通常用于维护序列 \(a_{1...n}\) 内,对于每个元素,在该元素前/后满足特定比较关系的第一个元素的下标 \(f(i)\)

stack<int> s;
for (int i = n; i; --i)
{
    while (!s.empty() && a[s.top()] <= a[i])
        s.pop();
    res[i] = s.empty() ? 0 : s.top();
    s.push(i);
}

二维前缀和

for (int i = 1; i <= m; ++i)
{
	for (int j = 1; j <= n; ++j)
  	{
		pre[i][j] = pre[i - 1][j] + pre[i][j - 1] - pre[i - 1][j - 1] + nums[i][j];
  	}
}
// 异或版本
for (int i = 1; i <= m; ++i)
{
	for (int j = 1; j <= n; ++j)
	{
		pre[i][j] = pre[i - 1][j] ^ pre[i][j - 1] ^ pre[i - 1][j - 1] ^ nums[i][j];
	}
}

并查集

struct Dsu
{
	int pa[N];
	Dsu() {
		iota(pa, pa + N, 0);
	}
	int find(int u) {
		return pa[u] == u ? u : pa[u] = find(pa[u]);
	}
	void merge(int u, int v) {
		pa[find(u)] = find(v);
	}
} dsu;

动态规划

数字三角形模型:“多线程” 情况

一般使用 f[k][i1][i2] 表示 两个人从(1,1), (1,1) -> (i, k-i1), (i1, k - i2) 的最大值之和.

例见:
P1004 [NOIP2000 提高组] 方格取数 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
P1006 [NOIP2008 提高组] 传纸条

// P1006 [NOIP2008 提高组] 传纸条
for (int k = 1; k <= n + m; ++k)
{
    for (int i1 = 1; i1 <= n; ++i1)
    {
        for (int i2 = 1; i2 <= n; ++i2)
        {
            int j1 = k - i1, j2 = k - i2;
            if (j1 < 1 || j1 > m || j2 < 1 || j2 > m)
                continue;
            int w = (j1 == j2) ? grid[i1][j1] : grid[i1][j1] + grid[i2][j2];
            for (int fi : {f[k - 1][i1][i2], f[k - 1][i1 - 1][i2], f[k - 1][i1][i2 - 1], f[k - 1][i1 - 1][i2 - 1]})
            {
                f[k][i1][i2] = max(f[k][i1][i2], fi + w);
            }
        }
    }
}

cout << f[n + m][n][n] << endl;

LIS 问题

\(dp_i\) 为所有的长度为 \(i\) 的不下降子序列的末尾元素的最小值,\(len\) 为子序列的长度。

memset(dp, 0x3f, sizeof(dp));
for (int i = 1; i <= n; ++i)
{
    if (dp[len] < a[i])
        dp[++len] = a[i];
    else
        *lower_bound(dp + 1, dp + len + 1, a[i]) = a[i];
}
cout << len << endl;

LCS:最长公共子序列到 LIS 的转化

现已知:

A:3 2 1 4 5
B:1 2 3 4 5

我们不妨给它们重新标个号:把3标成a,把2标成b,把1标成c……于是变成:

A: a b c d e
B: c b a d e

这样标号之后,LCS长度显然不会改变。但是出现了一个性质:两个序列的子序列,一定是A的子序列。而A本身就是单调递增的。因此这个子序列是单调递增的。换句话说,只要这个子序列在B中单调递增,它就是A的子序列。

#include <bits/stdc++.h>
using namespace std;

int n;
const int N = 100010;
int a[N], b[N], dp[N];

int main()
{
	ios::sync_with_stdio(false); cin.tie(nullptr);
	cin >> n;
	for (int i = 1; i <= n; ++i)
	{
		int ai; cin >> ai;
		a[ai] = i;
	}
	for (int i = 1; i <= n; ++i)
	{
		int bi; cin >> bi;
		b[i] = a[bi];
	}
	
	int len = 0;
	memset(dp, 0x3f, sizeof(dp));
	for (int i = 1; i <= n; ++i)
	{
		int pos = int(lower_bound(dp + 1, dp + len + 1, b[i]) - dp);
		len = max(pos, len);
		dp[pos] = b[i];
	}
	cout << len << endl;
}

背包问题: 多重背包的二进制优化问题

参考:OI WIKI
见例:P1833 樱花 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

使用二进制优化的基本思路在于:将每个物品按照其数量拆分成若干个不同的子物品,每个子物品的数量是2的幂次方。从而可以将多重背包问题转换为多个0/1背包问题进行处理。时间复杂度从 \(O(n \times m \times k)\) 降为 \(O(n \times m \times \log k)\)

	for (int i = 1; i <= n; ++i)
	{
		int t, c, p; scanf("%lld %lld %lld", &t, &c, &p);
		if (p == 0) p = 10000;
		for (int k = 1; k <= p; k *= 2)
		{
			++cnt;
			w[cnt] = t * k;
			v[cnt] = c * k;
			p -= k;
		}
		if (p > 0)
		{
			++cnt;
			w[cnt] = t * p;
			v[cnt] = c * p;
		}
	}
	
	for (int i = 1; i <= cnt; ++i)
	{
		for (int j = T; j >= w[i]; --j)
		{
			f[j] = max(f[j], f[j - w[i]] + v[i]);
		}
	}

区间动态规划

例见:
https://www.luogu.com.cn/problem/P1775
https://www.luogu.com.cn/problem/P1880
https://leetcode.cn/problems/burst-balloons/description/

区间 DP 有以下特点:

  1. 合并:即将两个或多个部分进行整合,当然也可以反过来;
  2. 特征:能将问题分解为能两两合并的形式;
  3. 求解:对整个问题设最优值,枚举合并点,将问题分解为左右两个部分,最后合并两个部分的最优值得到原问题的最优值。
	// 最外层枚举区间长度有小到大(对应上文小石子堆合成大石子堆),
	// 第二层枚举区间起点,最后一层枚举中间点
	for (int len = 2; len <= n; ++len)
	{
		for (int i = 1; i <= n - len + 1; ++i)
		{
			int j = i + len - 1;
			for (int k = i; k < j; ++k)
			{
				dp[i][j] = 
					min(dp[i][j], dp[i][k] + dp[k + 1][j] + pre[j] - pre[i - 1]);
			}
		}
	}
	cout << dp[1][n] << endl;

图论

多源最短路

适用于任何图,不管有向无向,边权正负,但是最短路必须存在。(不能有个负环)

for (k = 1; k <= n; k++) 
{
	for (x = 1; x <= n; x++) 
	{
		for (y = 1; y <= n; y++) 
		{
			f[x][y] = min(f[x][y], f[x][k] + f[k][y]);
	    }
	}
}

单源正权最短路

最短路一般使用 Dijkstra 的优先队列优化实现.

struct Node {
	int u, dis;
	bool operator>(const Node &rhs) const {
		return this->dis > rhs.dis;
	}
};
priority_queue<Node, vector<Node>, greater<Node>> pq;
int dist[N];
bool vis[N];

void dijkstra(int s)
{
	memset(dist, 0x3f, sizeof(dist));
	pq.push({s, 0});
	dist[s] = 0;
	
	while (!pq.empty())
	{
		int u = pq.top().u; pq.pop();
		if (vis[u]) continue;
		vis[u] = true;
		for (Edge e : adj[u]) 
		{
			int v = e.v, w = e.w;
			if (dist[v] > dist[u] + w) 
			{
				dist[v] = dist[u] + w;
				pq.push({v, dist[v]});
			}
		}	
	}
}

SPFA 与 负环

int dist[N], cnt[N];
bool vis[N];

bool spfa(int s)
{
	memset(dist, 0x3f, sizeof(dist));
	queue<int> q;
	dist[s] = 0, vis[s] = true;
	q.push(s);
	while (!q.empty())
	{
		int u = q.front(); q.pop();
		vis[u] = false;
		for (Edge e : adj[u])
		{
			int v = e.v, w = e.w;
			if (dist[v] > dist[u] + w)
			{
				dist[v] = dist[u] + w;
				cnt[v] = cnt[u] + 1;
				// 在不经过负环的情况下,最短路至多经过 n - 1 条边 
				// 因此如果经过了多于 n 条边,一定说明经过了负环
				if (cnt[v] >= n) return false;
				if (!vis[v])
				{
					q.push(v);
					vis[v] = true;
				}
			}
		}
	}
	return true;
}

Kruskal 与 最小生成树(MST)

#include <bits/stdc++.h>
using namespace std;

int n, m;
const int M = 200010, N = 5010; 
struct Edge {
    int u, v, w;
    bool operator<(const Edge &e) const {
        return this->w < e.w;
    }
} es[M];

struct Dsu {
    int pa[N];
    Dsu() { iota(pa, pa + N, 0); }
    int find(int x) { return x == pa[x] ? x : pa[x] = find(pa[x]); }
    void merge(int x, int y) { pa[find(x)] = find(y); }
} dsu;

int main()
{
    ios::sync_with_stdio(false); cin.tie(nullptr);
    cin >> n >> m;
    for (int i = 1; i <= m; ++i)
    {
        int u, v, w; cin >> u >> v >> w;
        es[i] = {u, v, w};
    }
    sort(es + 1, es + m + 1);
    int ans = 0, cnt = 0;
    for (int i = 1; i <= m; ++i)
    {
        int u = dsu.find(es[i].u), v = dsu.find(es[i].v);
        if (u != v)
        {
            dsu.merge(u, v);
            ans += es[i].w;
            ++cnt;
        }
    }
    if (cnt != n - 1) cout << "orz\n";
    else cout << ans << endl;
}

LCA

倍增算法模板

例见:P3379 【模板】最近公共祖先(LCA)

const int MAXD = 20;
int depth[N];
int fa[N][MAXD];

void bfs(int s)
{
	memset(depth, 0x3f, sizeof(depth));
	queue<int> q;
	q.push(s); depth[0] = 0, depth[s] = 1;
	while (!q.empty())
	{
		int u = q.front(); q.pop();
		for (int v : adj[u])
		{
			if (depth[v] > depth[u] + 1)
			{
				depth[v] = depth[u] + 1;
				fa[v][0] = u;
				for (int d = 1; d < MAXD; ++d)
					fa[v][d] = fa[fa[v][d - 1]][d - 1];
				q.push(v);
			}
		}
	}
}

int lca(int x, int y)
{
	if (depth[x] < depth[y]) swap(x, y);
	for (int d = MAXD - 1; d >= 0; --d)
	{
		if (depth[fa[x][d]] >= depth[y])
			x = fa[x][d];
	}
	if (x == y) return x;
	for (int d = MAXD - 1; d >= 0; --d)
	{
		if (fa[x][d] != fa[y][d])
		{
			x = fa[x][d];
			y = fa[y][d];
		}
	}
	return fa[x][0];
}

Tarjan 离线 LCA

tarjan 离线做法很大程度取决问题所求,通常场景为需要根据多个询问指定的两个节点的 LCA 以推导结果的情况。

例如,在边权图内求解任意两点的最短路径:

\[dis_{i,j} = dis_{root,i} + dis_{root,j} - 2 \times dis_{root, lca(i, j)} \]

\(dis_{root,u}\) 可以通过 DFS 预处理求出。而 \(lca(i, j)\) 则在 tarjan 递归中可以得知,见下:

struct Edge {
	int u, w;
};
vector<Edge> adj[N];

struct Query {
	int v, id;
};
vector<Query> qe[N];

struct Dsu
{
	int pa[N];
	Dsu() {
		iota(pa, pa + N, 0);
	}
	int find(int u) {
		return pa[u] == u ? u : pa[u] = find(pa[u]);
	}
	void merge(int u, int v) {
		pa[find(u)] = find(v);
	}
} dsu;

void dfs(int u, int fa)
{
	for (auto [v, w] : adj[u])
	{
		if (v == fa) continue;
		dis[v] = dis[u] + w;
		dfs(v, u);
	}
}

void tarjan(int u, int fa)
{
	st[u] = true;
	for (auto [u, w] : adj[u])
	{
		if (v != fa)
			tarjan(v, u);
	}

	for (Query q : qe[u])
	{
		int v = q.v, id = q.id;
		if (st[v])
		{
			int anc = dsu.find(v);
			res[id] = dis[u] + dis[v] - 2 * dis[anc];
		}
	}

	dsu.pa[u] = fa;
}

如果给定的为点权,则额外注意需要加上 LCA 的点权,因为在LCA本身也包含在 \(x \rightarrow y\) 的路径之中。

\[dis_{i,j} = dis_{root,i} + dis_{root,j} - 2 \times dis_{root, lca(i, j)} + pw_{lca(i, j)} \]

线段树

线段树可以在 \(O(\log N)\) 的时间复杂度内实现单点修改、区间修改、区间查询(区间求和,求区间最大值,求区间最小值)等操作。

无懒标记的单点修改

struct Node
{
    int l, r, mx;
};

struct Seg
{
    Node tr[M * 4];
    void build(int u, int l, int r)
    {
        tr[u].l = l, tr[u].r = r;
        // 叶子节点
        if (l == r)
            return;
        int mid = (l + r) >> 1;
        build(u << 1, l, mid);
        build(u << 1 | 1, mid + 1, r);
    }
    // 由子节点计算父节点
    void pushup(int u)
    {
        tr[u].mx = max(tr[u << 1].mx, tr[u << 1 | 1].mx);
    }
    // 获取 [l, r] 的目标值
    int query(int u, int l, int r)
    {
        // 当前节点的范围已被 [l, r] 包括在内
        if (tr[u].l >= l && tr[u].r <= r)
            return tr[u].mx;
        // [l, r] 必定会与我们递归的节点 u 范围有交集
        int res = INT_MIN, mid = (tr[u].l + tr[u].r) >> 1;
        if (l <= mid)
            res = query(u << 1, l, r);
        if (r > mid)
            res = max(res, query(u << 1 | 1, l, r));
        return res;
    }
    // 将位置 x 的元素改为 v
    void modify(int u, int x, int v)
    {
        // 已位于要修改的叶子节点
        if (tr[u].l == x && tr[u].r == x)
            tr[u].mx = v;
        else
        {
            int mid = (tr[u].l + tr[u].r) >> 1;
            if (x <= mid)
                modify(u << 1, x, v);
            else
                modify(u << 1 | 1, x, v);
            pushup(u);
        }
    }
} seg;

懒标记:区间修改

struct Node
{
    int l, r;
    // add 为懒标记
    // 注意:add 没有包含本身节点
    ll sum, add;
} tr[N * 4];

struct SegTree {
	Node tr[N << 2];
	void build(int u, int l, int r) {
		tr[u].l = l, tr[u].r = r;
		if (l == r) {
			tr[u].sum = a[l];
			tr[u].add = 0;
			return;
		}
		int mid = l + r >> 1;
		build(u << 1, l, mid);
		build(u << 1 | 1, mid + 1, r);
		pushup(u);
	}
	void pushup(int u) {
		tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
	}
	void pushdown(int u) {
		Node &root = tr[u], &left = tr[u << 1], &right = tr[u << 1 | 1];
		if (root.add) {
			left.add += root.add;
        	left.sum += (left.r - left.l + 1) * root.add;
        	right.add += root.add;
        	right.sum += (right.r - right.l + 1) * root.add;
        	root.add = 0;
		}
	}
	void modify(int u, int l, int r, int d) {
		auto &root = tr[u];
		if (root.l >= l && root.r <= r) {
			root.add += d;
			root.sum += (root.r - root.l + 1) * d;
		} else {
			pushdown(u);
			int mid = root.l + root.r >> 1;
			if (l <= mid) modify(u << 1, l, r, d);
			if (r > mid) modfiy(u << 1 | 1, l, r, d);
			pushup(u);
		}
	}
	ll query(int u, int l, int r) {
		auto &root = tr[u];
		if (root.l >= l && root.r <= r) return root.sum;
		pushdown(u);
		int mid = (root.l + root.r) >> 1;
		ll res = 0;
		if (l <= mid) res += query(u << 1, l, l);
		if (r > mid) res += query(u << 1 | 1, l, r);
		return res;
	}
} tr;

数论

线性素数筛

bool isprime[N];
int prime[N], np, pmx[N];

void lineve()
{
    for (int i = 2; i <= n; ++i) isprime[i] = true;
    for (int i = 2; i <= n; ++i)
    {
        if (isprime[i]) prime[++np] = i;
        for (int p = 1; p <= np && i * prime[p] <= n; ++p)
        {
            isprime[i * prime[p]] = false;
            if (i % prime[p] == 0) break;
        }
    }
}

快速幕

ll fpow(ll x, ll y, ll mod)
{
	x %= mod;
	ll res = 1;
	while (y > 0)
	{
		if (y & 1) res = res * x % mod;
		x = x * x % mod;
		y >>= 1;	
	}
	return res;
}

其它 & 杂记

获取 \(a/b\) 小数点后第 n 位

ll getpp(ll a, ll b, ll n) {
	return a * fpow(10, n - 1, b) * 10 / b % 10; 
}

posted on 2024-05-26 16:22  Himu  阅读(35)  评论(0编辑  收藏  举报