模板
树状数组 1:
支持单点加和,单点修改,区间求和,区间求最大值,时间复杂度
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 100005; // 根据数据范围调整,假设n <= 1e5
const ll INF = 1LL << 60; // 用于最大值的初始负无穷,防止溢出
ll tree_sum[MAXN * 4]; // 线段树数组,维护范围和
ll tree_max[MAXN * 4]; // 线段树数组,维护范围最大值
int n; // 数组大小
// 构建线段树,初始值从数组a[1..n]中读取
// 参考文档中的build函数,用于初始化sum和maxx[[6]][doc_6]
void build(int o, int l, int r, ll a[]) {
if (l == r) {
tree_sum[o] = a[l]; // 叶节点:和等于值本身
tree_max[o] = a[l]; // 叶节点:最大值等于值本身
return;
}
int mid = (l + r) >> 1; // 取中点
build(o * 2, l, mid, a); // 递归左子树
build(o * 2 + 1, mid + 1, r, a); // 递归右子树
tree_sum[o] = tree_sum[o * 2] + tree_sum[o * 2 + 1]; // 上推:和为子树和之和
tree_max[o] = max(tree_max[o * 2], tree_max[o * 2 + 1]); // 上推:最大值为子树最大值之max
}
// op3: 单点加k - 在位置pos加上val(支持负数,相当于减)
void update_add(int o, int l, int r, int pos, ll val) {
if (l == r) {
tree_sum[o] += val; // 叶节点:和增加val
tree_max[o] += val; // 叶节点:最大值增加val
return;
}
int mid = (l + r) >> 1;
if (pos <= mid) update_add(o * 2, l, mid, pos, val); // 向左子树递归
else update_add(o * 2 + 1, mid + 1, r, pos, val); // 向右子树递归
tree_sum[o] = tree_sum[o * 2] + tree_sum[o * 2 + 1]; // 上推和
tree_max[o] = max(tree_max[o * 2], tree_max[o * 2 + 1]); // 上推最大值
}
// op4: 单点修改为k - 将位置pos的值设为val
// 参考文档中的update函数,用于单点设置值[[2]][doc_2][[6]][doc_6]
void update_set(int o, int l, int r, int pos, ll val) {
if (l == r) {
tree_sum[o] = val; // 叶节点:和设为val
tree_max[o] = val; // 叶节点:最大值设为val
return;
}
int mid = (l + r) >> 1;
if (pos <= mid) update_set(o * 2, l, mid, pos, val);
else update_set(o * 2 + 1, mid + 1, r, pos, val);
tree_sum[o] = tree_sum[o * 2] + tree_sum[o * 2 + 1]; // 上推和
tree_max[o] = max(tree_max[o * 2], tree_max[o * 2 + 1]); // 上推最大值
}
// op1: 范围求和 - 查询[lq, rq]的和
// 参考文档中的query2函数,用于范围sum查询[[2]][doc_2]
ll query_sum(int o, int l, int r, int lq, int rq) {
if (lq > r || rq < l) return 0; // 无交集,返回0
if (lq <= l && r <= rq) return tree_sum[o]; // 完全覆盖,返回节点和
int mid = (l + r) >> 1;
return query_sum(o * 2, l, mid, lq, rq) + query_sum(o * 2 + 1, mid + 1, r, lq, rq); // 递归求子树和并相加
}
// op2: 范围求最大值 - 查询[lq, rq]的最大值
// 参考文档中的query1函数,用于范围max查询[[2]][doc_2]
ll query_max(int o, int l, int r, int lq, int rq) {
if (lq > r || rq < l) return -INF; // 无交集,返回负无穷
if (lq <= l && r <= rq) return tree_max[o]; // 完全覆盖,返回节点最大值
int mid = (l + r) >> 1;
return max(query_max(o * 2, l, mid, lq, rq), query_max(o * 2 + 1, mid + 1, r, lq, rq)); // 递归求子树最大值并取max
}
int main() {
// 示例使用(详见下方说明)
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
ll a[MAXN];
for (int i = 1; i <= n; i++) cin >> a[i];
build(1, 1, n, a); // 构建树
int q;
cin >> q;// 查询数量
while (q--) {
int op;
cin >> op;
if (op == 1) { // 区间求和
int l, r;
cin >> l >> r;
cout << query_sum(1, 1, n, l, r) << endl;
} else if (op == 2) { // 区间求最大值
int l, r;
cin >> l >> r;
cout << query_max(1, 1, n, l, r) << endl;
} else if (op == 3) { // 单点加值
int pos;
cin >> pos;
ll val;
cin >> val;
update_add(1, 1, n, pos, val);
} else if (op == 4) { // 单点修改值
int pos;
cin >> pos;
ll val;
cin >> val;
update_set(1, 1, n, pos, val);
}
}
return 0;
}
树状数组 2:
支持区间求和及区间加值,时间复杂度
#include <bits/stdc++.h>
using namespace std;
/*
说明:
- 本程序用两个树状数组(BIT1、BIT2)实现:
1) 区间加法:对 [l, r] 加 k
2) 区间求和:求 [l, r] 之和
- 公式(经典做法):
对 [l, r] 加 k 等价于:
BIT1 加: add(BIT1, l, k), add(BIT1, r+1, -k)
BIT2 加: add(BIT2, l, k*(l-1)), add(BIT2, r+1, -k*r)
前缀和 prefix_sum(x) = sum(BIT1, x) * x - sum(BIT2, x)
区间和 range_sum(l, r) = prefix_sum(r) - prefix_sum(l-1)
- 下标统一为 1..n(1-based)
- 值域使用 long long
*/
// 全局变量:n 为数组大小,q 为操作数
int n, q;
vector<long long> BIT1, BIT2; // 两棵树状数组,大小开到 n+2
// 低位操作
inline int lowbit(int x) { return x & -x; }
// 在 BIT 上做单点增加:BIT[idx] += delta(延伸到后继节点)
void add(vector<long long>& BIT, int idx, long long delta) {
for (; idx <= n; idx += lowbit(idx)) {
BIT[idx] += delta;
}
}
// 求 BIT 的前缀和:sum(BIT, idx) = BIT[1] + ... + BIT[idx]
long long sum(const vector<long long>& BIT, int idx) {
long long res = 0;
for (; idx > 0; idx -= lowbit(idx)) {
res += BIT[idx];
}
return res;
}
// 对区间 [l, r] 每个元素都加上 k
void range_add(int l, int r, long long k) {
if (l > r) return;
// BIT1
add(BIT1, l, k);
if (r + 1 <= n) add(BIT1, r + 1, -k);
// BIT2
add(BIT2, l, k * (l - 1));
if (r + 1 <= n) add(BIT2, r + 1, -k * r);
}
// 求前缀和:a[1] + ... + a[x]
long long prefix_sum(int x) {
if (x <= 0) return 0;
long long s1 = sum(BIT1, x);
long long s2 = sum(BIT2, x);
return s1 * x - s2;
}
// 求区间和:a[l] + ... + a[r]
long long range_sum(int l, int r) {
if (l > r) return 0;
return prefix_sum(r) - prefix_sum(l - 1);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
// 输入格式:
// n q
// a1 a2 ... an
// 之后 q 行操作:
// 1 l r 查询区间和
// 2 l r k 区间加 k
if (!(cin >> n >> q)) return 0;
BIT1.assign(n + 2, 0);
BIT2.assign(n + 2, 0);
// 读取初始数组,并用区间加的形式构建
for (int i = 1; i <= n; ++i) {
long long x; cin >> x;
// 等价于对 [i, i] 加 x
range_add(i, i, x);
}
// 处理操作
for (int i = 0; i < q; ++i) {
int op; cin >> op;
if (op == 1) {
int l, r; cin >> l >> r;
cout << range_sum(l, r) << '\n';
} else if (op == 2) {
int l, r; long long k; cin >> l >> r >> k;
range_add(l, r, k);
}
// 其他 op 值忽略(或可加入校验)
}
return 0;
}
线段树 1:
支持区间求和,区间求最大值,区间加值,区间修改,时间复杂度
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 100010; // n的最大值+10
const ll INF = 1LL << 60; // 用于最大值查询的负无穷(比-1e18小)
ll a[MAXN]; // 初始数组
ll tree_sum[MAXN * 4]; // 线段树存储区间和
ll tree_max[MAXN * 4]; // 线段树存储区间最大值
ll lazy_add[MAXN * 4]; // 懒惰加法标记
ll lazy_set[MAXN * 4]; // 懒惰赋值标记(-1表示无赋值)
// 构建线段树
void build(int o, int l, int r) {
if (l == r) { // 叶子节点
tree_sum[o] = a[l];
tree_max[o] = a[l];
return;
}
int mid = (l + r) >> 1; // 等同于/2
build(o * 2, l, mid);
build(o * 2 + 1, mid + 1, r);
tree_sum[o] = tree_sum[o * 2] + tree_sum[o * 2 + 1]; // 上推和
tree_max[o] = max(tree_max[o * 2], tree_max[o * 2 + 1]); // 上推最大值
}
// 下推懒惰标记(先处理赋值,再处理加法)
void pushdown(int o, int l, int r) {
int mid = (l + r) >> 1;
if (lazy_set[o] != -1) { // 有赋值标记,优先应用(重置加法)
// 应用到左子树
tree_sum[o * 2] = lazy_set[o] * (mid - l + 1);
tree_max[o * 2] = lazy_set[o];
lazy_set[o * 2] = lazy_set[o];
lazy_add[o * 2] = 0; // 重置子树加法
// 应用到右子树
tree_sum[o * 2 + 1] = lazy_set[o] * (r - mid);
tree_max[o * 2 + 1] = lazy_set[o];
lazy_set[o * 2 + 1] = lazy_set[o];
lazy_add[o * 2 + 1] = 0;
// 清空当前标记
lazy_set[o] = -1;
lazy_add[o] = 0;
}
if (lazy_add[o] != 0) { // 有加法标记
// 应用到左子树
tree_sum[o * 2] += lazy_add[o] * (mid - l + 1);
tree_max[o * 2] += lazy_add[o];
lazy_add[o * 2] += lazy_add[o];
// 应用到右子树
tree_sum[o * 2 + 1] += lazy_add[o] * (r - mid);
tree_max[o * 2 + 1] += lazy_add[o];
lazy_add[o * 2 + 1] += lazy_add[o];
// 清空当前标记
lazy_add[o] = 0;
}
}
// 区间加k(op=3)
void update_add(int o, int l, int r, int ql, int qr, ll val) {
if (ql <= l && r <= qr) { // 完全覆盖
tree_sum[o] += val * (r - l + 1);
tree_max[o] += val;
lazy_add[o] += val;
return;
}
pushdown(o, l, r); // 下推标记
int mid = (l + r) >> 1;
if (ql <= mid) update_add(o * 2, l, mid, ql, qr, val);
if (qr > mid) update_add(o * 2 + 1, mid + 1, r, ql, qr, val);
tree_sum[o] = tree_sum[o * 2] + tree_sum[o * 2 + 1]; // 上推
tree_max[o] = max(tree_max[o * 2], tree_max[o * 2 + 1]);
}
// 区间赋值为k(op=4)
void update_set(int o, int l, int r, int ql, int qr, ll val) {
if (ql <= l && r <= qr) { // 完全覆盖
tree_sum[o] = val * (r - l + 1);
tree_max[o] = val;
lazy_set[o] = val;
lazy_add[o] = 0; // 重置加法
return;
}
pushdown(o, l, r); // 下推标记
int mid = (l + r) >> 1;
if (ql <= mid) update_set(o * 2, l, mid, ql, qr, val);
if (qr > mid) update_set(o * 2 + 1, mid + 1, r, ql, qr, val);
tree_sum[o] = tree_sum[o * 2] + tree_sum[o * 2 + 1]; // 上推
tree_max[o] = max(tree_max[o * 2], tree_max[o * 2 + 1]);
}
// 查询区间和(op=1)
ll query_sum(int o, int l, int r, int ql, int qr) {
if (ql > r || qr < l) return 0; // 无交集
if (ql <= l && r <= qr) return tree_sum[o]; // 完全覆盖
pushdown(o, l, r); // 下推标记
int mid = (l + r) >> 1;
ll res = 0;
if (ql <= mid) res += query_sum(o * 2, l, mid, ql, qr);
if (qr > mid) res += query_sum(o * 2 + 1, mid + 1, r, ql, qr);
return res;
}
// 查询区间最大值(op=2)
ll query_max(int o, int l, int r, int ql, int qr) {
if (ql > r || qr < l) return -INF; // 无交集,返回负无穷
if (ql <= l && r <= qr) return tree_max[o]; // 完全覆盖
pushdown(o, l, r); // 下推标记
int mid = (l + r) >> 1;
ll res = -INF;
if (ql <= mid) res = max(res, query_max(o * 2, l, mid, ql, qr));
if (qr > mid) res = max(res, query_max(o * 2 + 1, mid + 1, r, ql, qr));
return res;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, q;
cin >> n >> q;
for (int i = 1; i <= n; i++) cin >> a[i];
memset(lazy_add, 0, sizeof(lazy_add)); // 初始化加法为0
memset(lazy_set, -1, sizeof(lazy_set)); // 初始化赋值为-1(无)
build(1, 1, n); // 构建树
while (q--) {
int op;
cin >> op;
if (op == 1) { // 范围求和
int l, r;
cin >> l >> r;
cout << query_sum(1, 1, n, l, r) << '\n';
} else if (op == 2) { // 范围求最大值
int l, r;
cin >> l >> r;
cout << query_max(1, 1, n, l, r) << '\n';
} else if (op == 3) { // 区间加k
int l, r;
ll k;
cin >> l >> r >> k;
update_add(1, 1, n, l, r, k);
} else if (op == 4) { // 区间赋值为k
int l, r;
ll k;
cin >> l >> r >> k;
update_set(1, 1, n, l, r, k);
}
}
return 0;
}
ST 表 1
支持末尾添加数字,区间求最大值和区间求最小值
#include <bits/stdc++.h>
using namespace std;
// 全局变量
static vector<vector<long long>> stMax; // stMax[k][i] 表示区间 [i, i+2^k-1] 的最大值
static vector<vector<long long>> stMin; // stMin[k][i] 表示区间 [i, i+2^k-1] 的最小值
static vector<int> lg = {0, 0}; // lg[n] = floor(log2(n)),至少有下标到 1
static int n = 0; // 当前数组长度(1-based 查询用)
// 确保 lg 数组有到 to 的取值
void ensureLogSize(int to) {
if ((int)lg.size() > to) return;
int old = (int)lg.size() - 1;
lg.resize(to + 1);
for (int i = old + 1; i <= to; ++i) {
lg[i] = lg[i / 2] + 1;
}
}
// 追加一个元素 x 到末尾(新位置为 n+1)
void add(long long x) {
int newN = n + 1;
ensureLogSize(newN);
int needLevels = lg[newN] + 1; // 需要的层数:[0 .. floor(log2(newN))]
if ((int)stMax.size() < needLevels) {
stMax.resize(needLevels);
stMin.resize(needLevels);
}
// 第 0 层:区间长度 1
stMax[0].push_back(x);
stMin[0].push_back(x);
// 更高层:每次只需在每层末尾补一个新块(长度 2^k 且以 newN-2^k 为起点)
for (int k = 1; k < needLevels; ++k) {
int i = newN - (1 << k); // 新块起点(0-based)
long long aMax = stMax[k - 1][i];
long long bMax = stMax[k - 1][i + (1 << (k - 1))];
stMax[k].push_back(max(aMax, bMax));
long long aMin = stMin[k - 1][i];
long long bMin = stMin[k - 1][i + (1 << (k - 1))];
stMin[k].push_back(min(aMin, bMin));
}
n = newN;
}
// 查询区间 [l, r](1-based,且保证 1 <= l <= r <= n)最大值
long long queryMax(int l, int r) {
int len = r - l + 1;
int k = lg[len];
int i1 = l - 1; // 左块起点(0-based)
int i2 = r - (1 << k); // 右块起点(0-based)
return max(stMax[k][i1], stMax[k][i2]);
}
// 查询区间 [l, r](1-based)最小值
long long queryMin(int l, int r) {
int len = r - l + 1;
int k = lg[len];
int i1 = l - 1;
int i2 = r - (1 << k);
return min(stMin[k][i1], stMin[k][i2]);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
long long a[n+1];
for (int i = 1;i <= n; ++i) {
cin >> a[i];
add(a[i]);
}
int q;
cin >> q;
while (q--) {
int op;
cin >> op;
if (op == 1) {
long long k;
cin >> k;
add(k);
} else if (op == 2) {
int l, r;
cin >> l >> r;
if (l > r) swap(l, r);
cout << queryMax(l, r) << '\n';
} else if (op == 3) {
int l, r;
cin >> l >> r;
if (l > r) swap(l, r);
cout << queryMin(l, r) << '\n';
}
}
return 0;
}
字典树 1:
#include <bits/stdc++.h>
using namespace std;
/*
字典树(Trie)说明:
- 每个节点维护:
- passCnt[u]: 有多少个插入的模式串经过该节点(即以该节点对应的前缀开头)。
- nxt[u]: 该节点经由某个字符可到达的子节点编号(使用 unordered_map<char,int> 存)。
- 根节点编号为 0,对应空前缀。
- 插入一个模式串 s 时:从根出发,每到达一个节点(包含根和沿途新老节点),passCnt++。
- 查询一个文本串 t 时:从根沿 t 的字符走;若途中断边则答案为 0;否则到达节点 u,答案为 passCnt[u]。
复杂度:
- 设所有模式串总长度为 L,总查询串长度为 T。
- 时间复杂度约为 O(L + T),常数取决于哈希表。
- 空间复杂度 O(节点数 + 边数) ≈ O(L)。
*/
// 全局容器:不使用 struct/class
static vector<unordered_map<char, int>> nxt; // nxt[u][c] = v
static vector<long long> passCnt; // 节点 u 的前缀通过计数
// 新建一个空节点并返回其编号
int new_node() {
nxt.emplace_back(); // 空的子边哈希表
passCnt.emplace_back(0); // 初始计数为 0
return (int)nxt.size() - 1;
}
// 初始化整棵 Trie(清空并创建根)
void init_trie(size_t reserve_nodes = 1) {
nxt.clear();
passCnt.clear();
nxt.reserve(reserve_nodes); // 预留节点容量(可选)
passCnt.reserve(reserve_nodes); // 预留节点容量(可选)
new_node(); // 建立根节点,编号 0
}
// 插入一个模式串 s(区分大小写,支持任意可读字符)
void trie_insert(const string& s) {
int u = 0;
// 根节点代表空前缀,也要计一次(空串是所有字符串的前缀)
passCnt[u]++;
for (char c : s) {
auto it = nxt[u].find(c);
if (it == nxt[u].end()) {
int v = new_node();
nxt[u][c] = v;
u = v;
} else {
u = it->second;
}
passCnt[u]++; // 该节点对应的前缀又多了一个模式串
}
}
// 查询:有多少模式串以 t 为前缀
long long trie_query_prefix_count(const string& t) {
int u = 0;
for (char c : t) {
auto it = nxt[u].find(c);
if (it == nxt[u].end()) {
return 0; // 路断了,说明没有任何模式串以 t 为前缀
}
u = it->second;
}
return passCnt[u];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, q;
if (!(cin >> n >> q)) return 0;
vector<string> patterns(n);
size_t total_len = 0;
for (int i = 0; i < n; ++i) {
cin >> patterns[i];
total_len += patterns[i].size();
}
// 预估节点数上界:1(根) + 所有模式串总长度
init_trie(total_len + 1);
// 建树
for (const auto& s : patterns) {
trie_insert(s);
}
// 逐个查询
while (q--) {
string t;
cin >> t;
cout << trie_query_prefix_count(t) << '\n';
}
return 0;
}
字典树 2:
支持查询求数组中两两元素异或的最大值。
#include <bits/stdc++.h>
using namespace std;
struct BinaryTrie {
// 每个节点有两个孩子(0/1),存子节点下标,-1 表示不存在
vector<array<int, 2>> nxt;
int maxBit; // 从 maxBit 到 0 遍历位
BinaryTrie(int maxBit_) : maxBit(maxBit_) {
nxt.clear();
nxt.push_back({-1, -1}); // 根节点
}
void insert(uint64_t x) {
int node = 0;
for (int b = maxBit; b >= 0; --b) {
int bit = (x >> b) & 1ULL;
if (nxt[node][bit] == -1) {
nxt[node][bit] = (int)nxt.size();
nxt.push_back({-1, -1});
}
node = nxt[node][bit];
}
}
// 在当前 Trie 中,找到与 x 异或结果最大的值,并返回该最大异或值
uint64_t queryMaxXor(uint64_t x) const {
int node = 0;
uint64_t res = 0;
for (int b = maxBit; b >= 0; --b) {
int bit = (x >> b) & 1ULL;
int want = bit ^ 1; // 希望走相反位以增大异或
if (nxt[node][want] != -1) {
res |= (1ULL << b);
node = nxt[node][want];
} else {
node = nxt[node][bit];
}
if (node == -1) break; // 空 Trie 安全防护
}
return res;
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N;
if (!(cin >> N)) return 0;
vector<uint64_t> a(N);
uint64_t mx = 0;
for (int i = 0; i < N; ++i) {
cin >> a[i];
mx = max(mx, a[i]);
}
if (N < 2) {
cout << 0 << '\n';
return 0;
}
// 计算需要的最高位(根据数组最大值自适应)
int maxBit = (mx == 0 ? 0 : 63 - __builtin_clzll(mx));
BinaryTrie trie(maxBit);
uint64_t ans = 0;
// 先插入第一个,再从第二个开始:保证 l < r(不会与自身配对)
trie.insert(a[0]);
for (int i = 1; i < N; ++i) {
ans = max(ans, trie.queryMaxXor(a[i]));
trie.insert(a[i]);
}
cout << ans << '\n';
return 0;
}
dfs bfs模板
#include <bits/stdc++.h>
using namespace std;
/*
深度优先搜索(DFS)
参数说明:
- u: 当前访问的节点
- g: 图的邻接表(1..n)
- vis: 访问标记数组(1..n),1 表示已访问
- order: 记录 DFS 的访问顺序
设计要点:
- 预先保证 g[u] 已经按升序排序,这样遇到多个可访问的点时,会优先访问编号更小的点。
*/
void dfs(int u, const vector<vector<int>>& g, vector<int>& vis, vector<int>& order) {
vis[u] = 1; // 标记当前节点已访问
order.push_back(u); // 记录访问顺序
for (int v : g[u]) { // 按升序遍历邻居
if (!vis[v]) {
dfs(v, g, vis, order);
}
}
}
/*
广度优先搜索(BFS)
参数说明:
- start: 起点(本题默认从 1 开始,因为图保证联通)
- g: 图的邻接表(1..n)
返回值:
- 按访问顺序记录的节点序列
设计要点:
- 为确保同一层内先访问编号更小的节点,要求 g[u] 已升序;
将未访问的邻居按升序入队即可实现“同一时间访问,先输出较小点”的要求。
*/
vector<int> bfs(int start, const vector<vector<int>>& g) {
int n = (int)g.size() - 1; // g 按 1..n 存储,因此 size() - 1 是节点数
vector<int> vis(n + 1, 0);
queue<int> q;
vector<int> order;
vis[start] = 1;
q.push(start);
while (!q.empty()) {
int u = q.front();
q.pop();
order.push_back(u);
// g[u] 已经升序,因此入队顺序保证了同层由小到大
for (int v : g[u]) {
if (!vis[v]) {
vis[v] = 1; // 入队前标记,避免同一层被重复入队
q.push(v);
}
}
}
return order;
}
/*
辅助函数:按空格分隔输出序列(末尾不多输出空格)
*/
void print_sequence(const vector<int>& seq) {
for (size_t i = 0; i < seq.size(); ++i) {
if (i) cout << ' ';
cout << seq[i];
}
cout << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
if (!(cin >> n >> m)) return 0;
// 建图(无向图),使用 1..n 的邻接表
vector<vector<int>> g(n + 1);
for (int i = 0; i < m; ++i) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
// 为满足“多个点同时可访问时先访问编号更小的点”,对邻接表升序排序。
// 同时去重可避免多重边造成的重复判断(不影响正确性,只是更高效)。
for (int u = 1; u <= n; ++u) {
auto& adj = g[u];
sort(adj.begin(), adj.end());
adj.erase(unique(adj.begin(), adj.end()), adj.end());
}
// 由于题目说明图是联通的,默认从 1 号点开始遍历
int start = 1;
// DFS
vector<int> vis(n + 1, 0);
vector<int> dfs_order;
dfs(start, g, vis, dfs_order);
// BFS
vector<int> bfs_order = bfs(start, g);
// 输出:第一行 DFS 顺序,第二行 BFS 顺序
print_sequence(dfs_order);
print_sequence(bfs_order);
return 0;
}
/*
使用示例:
输入:
5 5
1 2
1 3
2 4
3 4
4 5
输出可能为:
1 2 4 3 5
1 2 3 4 5
说明:
- DFS:1 -> 2 -> 4 -> 3 -> 5(因邻接表排序,优先访问编号更小的邻居)
- BFS:层序从 1 开始,每层内按照节点编号从小到大入队访问
*/
快速幂模板
int qp(int a,int b,int mod){
int ans=1;
while(b>0){
if(b&1)ans=(ans*a)%mod;
a=(a*a)%mod;
b>>=1;
}
return ans%mod;
}
DSU
class DSU {
private:
std::vector<int> parent;
std::vector<int> sz;
public:
DSU(int n) {
parent.resize(n);
std::iota(parent.begin(), parent.end(), 0);
sz.assign(n, 1);
}
int find(int i) {
if (parent[i] == i) {
return i;
}
return parent[i] = find(parent[i]);
}
void unite(int i, int j) {
int root_i = find(i);
int root_j = find(j);
if (root_i != root_j) {
if (sz[root_i] < sz[root_j]) {
std::swap(root_i, root_j);
}
parent[root_j] = root_i;
sz[root_i] += sz[root_j];
}
}
};
Bellman Ford
/*
Bellman-Ford(SPFA 队列优化)模板 —— 邻接表版(1..n)
参数说明:
- s: 源点(默认 1,可按需修改或从输入读取)
- g: 图的邻接表(1..n),g[u] = vector<pair<int,int>>,元素 (v, w) 表示边 u->v、权重 w
返回值:
- pair<bool, vector<long long>>
first = true 表示不存在从 s 可达的负环
first = false 表示存在从 s 可达的负环(此时 second 中的 d 可能无意义)
second = 最短路数组 d,若不可达则为 INF
设计要点:
- 仅当 d[v] 被成功松弛才将 v 入队,减少无效入队。
- inq[v] 标记 v 是否在队中,避免重复入队。
- cnt[v] 统计“v 被成功松弛”的次数;若 cnt[v] >= n,判定存在从 s 可达的负环。
- 时间复杂度:最坏 O(nm),实际常优于朴素 Bellman-Ford。
*/
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll INF = (ll)4e18;
pair<bool, vector<ll>> spfa(int s, const vector<vector<pair<int,int>>>& g) {
int n = (int)g.size() - 1; // g 按 1..n 存储
vector<ll> d(n + 1, INF); // 最短路
vector<int> inq(n + 1, 0); // 是否在队列中
vector<int> cnt(n + 1, 0); // 成功松弛次数
queue<int> q;
d[s] = 0;
q.push(s);
inq[s] = 1;
while (!q.empty()) {
int u = q.front();
q.pop();
inq[u] = 0;
// 扫描 u 的所有出边
for (auto [v, w] : g[u]) {
if (d[u] != INF && d[v] > d[u] + (ll)w) {
d[v] = d[u] + (ll)w;
// v 被成功松弛一次
cnt[v] += 1;
if (cnt[v] >= n) {
// 存在从 s 可达的负环
return {false, d};
}
if (!inq[v]) {
q.push(v);
inq[v] = 1;
}
}
}
}
return {true, d};
}
/*
辅助函数:按空格分隔输出最短路(INF 表示不可达)
*/
void print_d(const vector<ll>& d) {
int n = (int)d.size() - 1;
for (int i = 1; i <= n; ++i) {
if (i > 1) cout << ' ';
if (d[i] > INF / 2) cout << "INF";
else cout << d[i];
}
cout << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
if (!(cin >> n >> m)) return 0;
// 建图(有向图),使用 1..n 的邻接表
vector<vector<pair<int,int>>> g(n + 1);
for (int i = 0; i < m; ++i) {
int u, v, w;
cin >> u >> v >> w;
g[u].push_back({v, w});
// 若为无向图,请同时加入:g[v].push_back({u, w});
}
int s = 1; // 源点(可按需修改或从输入读取)
auto [ok, d] = spfa(s, g);
if (!ok) {
cout << "NEGATIVE CYCLE\n";
} else {
print_d(d);
}
return 0;
}
/*
使用示例:
输入:
5 7
1 2 2
1 3 5
2 3 -3
2 4 4
3 5 1
4 5 2
5 4 -1
输出可能为:
0 2 -1 6 0
或若存在从 1 可达的负环:
NEGATIVE CYCLE
说明:
- d[i] = 从源点 1 到 i 的最短路;不可达输出 INF。
- 将 s 修改为需要的源点,或自行从输入读入。
*/
Dijkstra
/*
Dijkstra 最短路(优先队列版)模板 —— 邻接表(1..n)
参数说明:
- s: 源点(默认 1,可按需修改或从输入读取)
- g: 图的邻接表(1..n),g[u] = vector<pair<int,int>>,元素 (v, w) 表示边 u->v、权重 w
返回值:
- vector<long long> d:最短路数组,d[i] = 从 s 到 i 的最短距离;不可达为 INF
设计要点:
- 使用最小堆(优先队列)按当前已知最短距离弹出节点,避免 O(nm) 的枚举,复杂度约 O((n+m) log n)
- 权重需满足非负(w >= 0);若存在负边,请改用 Bellman-Ford/SPFA
- vis[u](或称 st[u])用于标记最短路已确定的点,避免重复扩展
*/
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll INF = (ll)4e18;
/*
函数:dij
作用:从源点 s 计算到各点的最短路
*/
vector<ll> dij(int s, const vector<vector<pair<int,int>>>& g) {
int n = (int)g.size() - 1; // g 使用 1..n 存储
vector<ll> d(n + 1, INF); // 最短路
vector<int> vis(n + 1, 0); // 是否已确定最短路
// 小根堆:按 (距离, 节点) 最小优先
priority_queue<pair<ll,int>, vector<pair<ll,int>>, greater<pair<ll,int>>> pq;
d[s] = 0;
pq.push({0, s});
while (!pq.empty()) {
auto [du, u] = pq.top();
pq.pop();
if (vis[u]) continue; // 已处理过的点跳过
vis[u] = 1;
// 用 u 的出边尝试松弛邻居
for (auto [v, w] : g[u]) {
if (d[v] > du + (ll)w) {
d[v] = du + (ll)w;
pq.push({d[v], v});
}
}
}
return d;
}
/*
辅助函数:按空格分隔输出最短路(INF 表示不可达)
*/
void print_d(const vector<ll>& d) {
int n = (int)d.size() - 1;
for (int i = 1; i <= n; ++i) {
if (i > 1) cout << ' ';
if (d[i] > INF / 2) cout << "INF";
else cout << d[i];
}
cout << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
if (!(cin >> n >> m)) return 0;
// 建图(有向图),使用 1..n 的邻接表
vector<vector<pair<int,int>>> g(n + 1);
for (int i = 0; i < m; ++i) {
int u, v, w;
cin >> u >> v >> w;
g[u].push_back({v, w});
// 若为无向图,额外加入:g[v].push_back({u, w});
}
int s = 1; // 源点(可按需修改或从输入读取)
auto d = dij(s, g);
print_d(d);
return 0;
}
/*
使用示例:
输入:
5 7
1 2 2
1 3 5
2 3 1
2 4 4
3 5 1
4 5 2
2 5 7
输出可能为(从源点 1 出发):
0 2 3 6 4
说明:
- d[i] = 从源点 1 到 i 的最短路;不可达输出 INF。
- 若图是无向的,请在读入时对称加入两条边。
- Dijkstra 要求所有边权非负。
*/
Johnson
/*
Johnson 全源最短路模板 —— 邻接表(1..n)
参数说明:
- g: 图的邻接表(1..n),g[u] = vector<pair<int,long long>>,元素 (v, w) 表示边 u->v、权重 w
返回值:
- 若存在可达负环:输出 "NEGATIVE CYCLE"
- 否则:输出 n 行,每行 n 个数,第 i 行第 j 列为从 i 到 j 的最短距离;不可达为 INF
设计要点:
- 允许负权边,但不得存在可达负环。
- 使用 SPFA 计算势函数 h(相当于从超级源 0 到各点的最短路),以将边权重标定为非负:
w'(u,v) = w(u,v) + h[u] - h[v] >= 0
- 对每个源点运行 Dijkstra 于重标后的图,时间复杂度约 O(n (m log n))。
- 原距离恢复:d(u,v) = d'(u,v) - h[u] + h[v],其中 d' 为重标图上的最短路。
*/
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll INF = (ll)4e18;
/*
SPFA 求势函数 h(从超级源 0 出发)
做法等价于:给每个点加一条 0->v 权值 0 的边,然后跑一次 Bellman-Ford。
实现细节:
- 将所有节点初始化入队,h[v] 初值为 0。
- cnt[v] 统计 v 被成功松弛的次数;若 >= n 则存在负环。
返回:
- true 无负环,h 填好
- false 存在可达负环
*/
bool spfa0(const vector<vector<pair<int,ll>>>& g, vector<ll>& h) {
int n = (int)g.size() - 1;
h.assign(n + 1, 0);
vector<int> inq(n + 1, 1), cnt(n + 1, 0);
queue<int> q;
for (int i = 1; i <= n; ++i) q.push(i);
while (!q.empty()) {
int u = q.front(); q.pop();
inq[u] = 0;
for (auto [v, w] : g[u]) {
if (h[v] > h[u] + w) {
h[v] = h[u] + w;
cnt[v] += 1;
if (cnt[v] >= n) return false; // 可达负环
if (!inq[v]) {
q.push(v);
inq[v] = 1;
}
}
}
}
return true;
}
/*
Dijkstra(在重标后的图上进行,边权非负)
输入:
- s: 源点
- g: 原图邻接表
- h: 势函数(由 spfa0 计算)
返回:
- d': 从 s 到各点的重标最短路(若不可达为 INF)
转移:
- w'(u,v) = w(u,v) + h[u] - h[v]
*/
vector<ll> dij_rw(int s, const vector<vector<pair<int,ll>>>& g, const vector<ll>& h) {
int n = (int)g.size() - 1;
vector<ll> d(n + 1, INF);
using P = pair<ll,int>;
priority_queue<P, vector<P>, greater<P>> pq;
d[s] = 0;
pq.push({0, s});
while (!pq.empty()) {
auto [du, u] = pq.top(); pq.pop();
if (du != d[u]) continue; // 过期状态
for (auto [v, w] : g[u]) {
ll w2 = w + h[u] - h[v]; // 重标后的非负边权
if (w2 < 0) w2 = 0; // 数值稳健(理论上应 >= 0)
ll nd = du + w2;
if (nd < d[v]) {
d[v] = nd;
pq.push({nd, v});
}
}
}
return d;
}
/*
Johnson 主流程:
1) 用 spfa0 求势函数 h,检测负环;
2) 对每个源点 s,跑一次 Dijkstra(用重标权),得到 d';
3) 还原原图距离:ans[s][v] = d'[v] - h[s] + h[v]
返回:
- pair<bool, vector<vector<ll>>>:
first = true 无负环;second 为 n+1 x n+1 的距离矩阵(1..n 有效)
first = false 存在负环;second 为空
*/
pair<bool, vector<vector<ll>>> johnson(const vector<vector<pair<int,ll>>>& g) {
int n = (int)g.size() - 1;
vector<ll> h;
if (!spfa0(g, h)) return {false, {}};
vector<vector<ll>> ans(n + 1, vector<ll>(n + 1, INF));
for (int s = 1; s <= n; ++s) {
auto d2 = dij_rw(s, g, h); // d' on reweighted graph
for (int v = 1; v <= n; ++v) {
if (d2[v] >= INF / 2) ans[s][v] = INF;
else ans[s][v] = d2[v] - h[s] + h[v]; // 还原原图距离
}
}
return {true, ans};
}
/*
辅助输出:打印 n 行 n 列的距离矩阵
*/
void print_mat(const vector<vector<ll>>& a) {
int n = (int)a.size() - 1;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
if (j > 1) cout << ' ';
if (a[i][j] >= INF / 2) cout << "INF";
else cout << a[i][j];
}
cout << '\n';
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
if (!(cin >> n >> m)) return 0;
// 建图(有向图),使用 1..n 的邻接表
vector<vector<pair<int,ll>>> g(n + 1);
for (int i = 0; i < m; ++i) {
int u, v;
ll w;
cin >> u >> v >> w;
g[u].push_back({v, w});
// 若为无向图,额外加入:g[v].push_back({u, w});
}
auto [ok, dist] = johnson(g);
if (!ok) {
cout << "NEGATIVE CYCLE\n";
return 0;
}
print_mat(dist);
return 0;
}
/*
使用示例:
输入:
5 7
1 2 2
1 3 5
2 3 -3
2 4 4
3 5 1
4 5 2
2 5 7
可能输出(某一合法结果):
0 2 -1 6 0
INF 0 -3 4 -2
INF INF 0 INF 1
INF INF INF 0 2
INF INF INF INF 0
说明:
- 允许负边,但若存在可达负环会输出 NEGATIVE CYCLE。
- 对无向图请对称加入边。
- 若只需某些源点的最短路,可只对这些源点运行 Dijkstra。
*/
拓扑排序
/*
拓扑排序(Topological Sort)模板 —— 邻接表(1..n)
参数说明:
- g: 有向图的邻接表(1..n),g[u] 存储所有从 u 指向的邻居 v(边 u->v)
返回值:
- 若存在有向环:输出 "CYCLE"
- 否则:输出一个拓扑序列(若有多个,默认输出“字典序最小”的序列)
设计要点:
- 使用 Kahn 算法(基于入度):
1) 计算每个点的入度 deg[v];
2) 将所有入度为 0 的点入队(这里用小根堆以得到字典序最小的拓扑序);
3) 反复弹出一个入度为 0 的点 u,将其加入答案,并“删除”它的出边
(对应地将邻居 v 的入度减 1,若变为 0 则入队)。
- 若最终弹出的点不足 n 个,说明存在有向环(无法拓扑排序)。
- 复杂度:O(n + m),使用小根堆为 O((n + m) log n)。
*/
#include <bits/stdc++.h>
using namespace std;
/*
函数:topo
作用:对有向图 g 进行拓扑排序;若有环返回空序列
细节:
- 使用小根堆(优先队列)保证多解时输出字典序最小的拓扑序
*/
vector<int> topo(const vector<vector<int>>& g) {
int n = (int)g.size() - 1; // g 按 1..n 存储
vector<int> deg(n + 1, 0); // 入度数组
for (int u = 1; u <= n; ++u) {
for (int v : g[u]) deg[v] += 1;
}
// 小根堆:保证每次取编号最小的入度为 0 的点
priority_queue<int, vector<int>, greater<int>> pq;
for (int u = 1; u <= n; ++u) if (deg[u] == 0) pq.push(u);
vector<int> ord;
ord.reserve(n);
while (!pq.empty()) {
int u = pq.top(); pq.pop();
ord.push_back(u);
for (int v : g[u]) {
if (--deg[v] == 0) pq.push(v);
}
}
if ((int)ord.size() != n) {
// 存在有向环,无法拓扑排序
return {};
}
return ord;
}
/*
辅助函数:按空格分隔输出序列(末尾不多输出空格)
*/
void print_seq(const vector<int>& a) {
for (size_t i = 0; i < a.size(); ++i) {
if (i) cout << ' ';
cout << a[i];
}
cout << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
if (!(cin >> n >> m)) return 0;
// 建图(有向图),使用 1..n 的邻接表
vector<vector<int>> g(n + 1);
for (int i = 0; i < m; ++i) {
int u, v;
cin >> u >> v; // 边 u -> v
g[u].push_back(v);
}
auto ord = topo(g);
if (ord.empty()) {
cout << "CYCLE\n";
} else {
print_seq(ord);
}
return 0;
}
/*
使用示例:
输入:
6 6
1 2
1 3
3 4
2 4
4 5
2 6
可能输出(字典序最小的一个拓扑序):
1 2 3 4 6 5
说明:
- 若图中存在有向环,将输出:CYCLE
- 若不要求字典序最小,可将小根堆改为普通队列 queue<int>,其余逻辑不变。
*/
树的直径
/*
树的直径(Tree Diameter)模板 —— 邻接表(1..n)
参数说明:
- g: 无向树的邻接表(1..n),g[u] 存储所有 (v, w) 表示边 u—v,权重为 w
返回值:
- 直径的两个端点 (a, b) 以及直径长度 len
设计要点:
- “两次最远点”法:
1) 从任意点 s(常取 1)出发,找到最远点 a;
2) 从 a 出发,找到最远点 b,同时得到距离 len = dist(a, b);
则 (a, b) 为树的直径端点。
- 若是无权树,可将所有 w 设为 1;本模板统一支持带权/无权两种情况。
- 为避免深递归导致栈溢出,使用迭代 DFS。
- 复杂度 O(n)。
*/
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
/*
函数:far
作用:从起点 s 出发,沿树进行一次迭代 DFS,返回距离最远的节点及其距离
说明:
- 树上任意走法的路径唯一,因此首次访问即得到其唯一的路径距离
- par 数组用于记录父节点,避免走回头路
*/
pair<int, ll> far(int s, const vector<vector<pair<int, ll>>>& g) {
int n = (int)g.size() - 1;
vector<int> par(n + 1, -1);
vector<ll> dist(n + 1, -1);
stack<int> st;
par[s] = 0;
dist[s] = 0;
st.push(s);
while (!st.empty()) {
int u = st.top(); st.pop();
for (auto [v, w] : g[u]) {
if (par[v] == -1) { // 未访问
par[v] = u;
dist[v] = dist[u] + w;
st.push(v);
}
}
}
int best = s;
for (int i = 1; i <= n; ++i) {
if (dist[i] > dist[best]) best = i;
}
return {best, dist[best]};
}
/*
函数:diam
作用:计算树的直径端点与长度
返回:
- tuple<int,int,ll> = (a, b, len)
*/
tuple<int,int,ll> diam(const vector<vector<pair<int, ll>>>& g) {
int s = 1; // 任取起点(树保证连通)
int a = far(s, g).first; // 第一次:找到最远点 a
auto [b, len] = far(a, g); // 第二次:从 a 出发得到最远点 b 及距离 len
return {a, b, len};
}
/*
可选:输出 (a, b, len),其中 len 为直径长度
*/
void print_diam(int a, int b, ll len) {
// 统一以空格分隔;如需自定义格式可在此修改
cout << a << ' ' << b << ' ' << len << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
// 构建邻接表(无向树),使用 1..n
vector<vector<pair<int, ll>>> g(n + 1);
// 输入格式一(默认无权树):接下来的 n-1 行,每行 u v,边权视为 1
// 若为带权树,请将下面的读入替换为“输入格式二”
for (int i = 0; i < n - 1; ++i) {
int u, v;
cin >> u >> v;
ll w = 1; // 无权树:边权为 1
g[u].push_back({v, w});
g[v].push_back({u, w});
}
// 输入格式二(带权树):若你的数据有权重,请改用下面注释块(并注释掉上面的无权读入)
/*
for (int i = 0; i < n - 1; ++i) {
int u, v; ll w;
cin >> u >> v >> w;
g[u].push_back({v, w});
g[v].push_back({u, w});
}
*/
auto [a, b, len] = diam(g);
print_diam(a, b, len);
return 0;
}
/*
使用示例(无权):
输入:
5
1 2
2 3
2 4
4 5
输出(端点与直径长度,可能存在多个合法端点对):
3 5 3
说明:
- 直径长度为 3(边数),一个最远点对是 3 与 5。
- 若为带权树,将输入改为三元组 (u, v, w),并启用“输入格式二”。
*/
树的中心
/*
树的中心(Tree Center)模板 —— 邻接表(1..n)
参数说明:
- g: 无向树的邻接表(1..n),g[u] 存储所有与 u 相邻的点 v
返回值:
- 树的中心(可能 1 个或 2 个):若为 1 个中心返回 {c},若为 2 个中心返回 {c1, c2}
设计要点:
- 采用“剥叶法”(Kahn 思想):
1) 计算每个点的度数 deg[u],将所有度为 1 的点入队(即叶子);
2) 分层地剥去当前所有叶子,并将其相邻点的度数减 1;
3) 当剩余节点数 rem <= 2 时,队列中的点即为树的中心(1 或 2 个)。
- 复杂度 O(n);仅依赖度数与邻接表,不需要递归与长路径恢复。
*/
#include <bits/stdc++.h>
using namespace std;
/*
函数:center
作用:求树的中心(可能 1 或 2 个)
输入:
- g: 邻接表(1..n)
返回:
- vector<int>:按升序返回中心点编号(1 或 2 个)
*/
vector<int> center(const vector<vector<int>>& g) {
int n = (int)g.size() - 1; // g 按 1..n 存储
if (n <= 0) return {};
if (n == 1) return {1};
vector<int> deg(n + 1, 0);
queue<int> q;
for (int u = 1; u <= n; ++u) {
deg[u] = (int)g[u].size();
if (deg[u] <= 1) q.push(u); // 叶子或孤点(树中仅可能为叶子)
}
int rem = n; // 剩余未剥除的节点数量
while (rem > 2) {
int sz = (int)q.size(); // 当前层叶子数量
rem -= sz; // 剥去这一层所有叶子
while (sz--) {
int u = q.front(); q.pop();
for (int v : g[u]) {
if (--deg[v] == 1) q.push(v); // 新形成的叶子入队
}
}
}
// 此时队列中为 1 或 2 个中心
vector<int> cent;
while (!q.empty()) { cent.push_back(q.front()); q.pop(); }
sort(cent.begin(), cent.end());
cent.erase(unique(cent.begin(), cent.end()), cent.end());
return cent;
}
/*
辅助函数:按空格分隔输出序列(末尾不多输出空格)
*/
void print_seq(const vector<int>& a) {
for (size_t i = 0; i < a.size(); ++i) {
if (i) cout << ' ';
cout << a[i];
}
cout << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
// 建树(无向),使用 1..n 的邻接表
vector<vector<int>> g(n + 1);
for (int i = 0; i < n - 1; ++i) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
auto c = center(g);
print_seq(c);
return 0;
}
/*
使用示例:
输入:
5
1 2
2 3
2 4
4 5
可能输出(一个合法中心集合):
2 4
说明:
- 本例中直径长度为 3,中心为直径中点(若直径长度为奇数,则有 1 个中心;为偶数,则有 2 个中心)。
- 本模板针对无向连通无环图(树)。若输入不是树,行为未定义。
*/
LCA
/*
最近公共祖先(LCA, Lowest Common Ancestor)模板 —— 邻接表(1..n)
参数说明:
- g: 无向树的邻接表(1..n),g[u] 存储与 u 相邻的所有点 v
- rt: 根节点(默认 1,可根据需要指定或从输入读取)
返回值:
- 函数 lca(u, v) 返回节点 u 和 v 的最近公共祖先
设计要点:
- 二进制提升(倍增):
1) BFS/DFS 预处理 dep[u](深度)与 up[k][u](u 的第 2^k 个祖先);
2) 先把较深的点提到同一深度,再同时向上跳,O(log n) 求解 LCA。
- 复杂度:预处理 O(n log n),单次查询 O(log n)。
*/
#include <bits/stdc++.h>
using namespace std;
/*
函数:init
作用:用 BFS 预处理深度和倍增祖先表
输入:
- rt: 根节点
- g: 邻接表(1..n)
输出:
- dep: dep[u] 为从根到 u 的深度(根为 0)
- up: up[k][u] 为 u 的第 2^k 个祖先(根的祖先定义为根自己)
*/
void init(int rt, const vector<vector<int>>& g, vector<int>& dep, vector<vector<int>>& up) {
int n = (int)g.size() - 1;
int LG = 32 - __builtin_clz(n); // ceil(log2(n))
up.assign(LG, vector<int>(n + 1, rt));
dep.assign(n + 1, -1);
queue<int> q;
dep[rt] = 0;
up[0][rt] = rt;
q.push(rt);
// BFS 建树:确定每个点的父亲 up[0][u] 与深度 dep[u]
while (!q.empty()) {
int u = q.front(); q.pop();
for (int v : g[u]) {
if (dep[v] != -1) continue; // 已访问(已确定父亲)
dep[v] = dep[u] + 1;
up[0][v] = u;
q.push(v);
}
}
// 倍增:up[k][u] = up[k-1][ up[k-1][u] ]
for (int k = 1; k < LG; ++k) {
for (int u = 1; u <= n; ++u) {
up[k][u] = up[k-1][ up[k-1][u] ];
}
}
}
/*
函数:lift
作用:将节点 u 向上提升 dist 层(返回提升后的节点)
说明:
- 使用 up 表按二进制分解进行跳跃
*/
int lift(int u, int dist, const vector<vector<int>>& up) {
for (int k = 0; dist; ++k, dist >>= 1) {
if (dist & 1) u = up[k][u];
}
return u;
}
/*
函数:lca
作用:返回 u 与 v 的最近公共祖先
做法:
- 先把较深的点提升到相同深度;
- 若相等则为 LCA;
- 否则自高位到低位同时向上跳,最终返回其父亲。
*/
int lca(int u, int v, const vector<int>& dep, const vector<vector<int>>& up) {
if (dep[u] < dep[v]) swap(u, v);
int diff = dep[u] - dep[v];
u = lift(u, diff, up);
if (u == v) return u;
for (int k = (int)up.size() - 1; k >= 0; --k) {
if (up[k][u] != up[k][v]) {
u = up[k][u];
v = up[k][v];
}
}
return up[0][u];
}
/*
可选:若需要两点间距离(无权树按边数计),可用:
dist(u, v) = dep[u] + dep[v] - 2 * dep[lca(u, v)]
若为带权树,可在 BFS/DFS 时同步维护从根到每点的前缀权和 sum[u],
则加权距离 = sum[u] + sum[v] - 2 * sum[lca(u, v)]。
*/
/*
辅助函数:按行输出查询结果
*/
void print_ans(int x) {
cout << x << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, q;
if (!(cin >> n >> q)) return 0;
// 建树(无向),使用 1..n 的邻接表
vector<vector<int>> g(n + 1);
for (int i = 0; i < n - 1; ++i) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
int rt = 1; // 根(可根据题意修改或从输入读取)
vector<int> dep;
vector<vector<int>> up;
init(rt, g, dep, up);
// 处理 q 次查询:每次给出 u, v 输出 LCA(u, v)
while (q--) {
int u, v;
cin >> u >> v;
int a = lca(u, v, dep, up);
print_ans(a);
}
return 0;
}
/*
使用示例:
输入:
7 4
1 2
1 3
2 4
2 5
3 6
6 7
4 5
4 6
5 7
2 7
输出:
2
1
1
1
说明:
- 树为 1 为根的无向树(输入的是边,程序会以 1 为根进行预处理)。
- 每行对应一次 LCA 查询的答案。
*/
拓展欧拉定理
/*
扩展欧拉定理模板:计算 a^b mod m
参数说明:
- a, b, m(m >= 1)
返回值:
- a^b mod m
设计要点(扩展欧拉定理):
- 令 φ = phi(m)
- 若 gcd(a, m) == 1,则 a^b ≡ a^{b mod φ} (mod m)
- 若 gcd(a, m) != 1,则
当 b < φ 时:a^b ≡ a^b (mod m)(不做降幂)
当 b ≥ φ 时:a^b ≡ a^{(b mod φ) + φ} (mod m)
- 注意:m == 1 时,任何数 mod 1 为 0
实现细节:
- phi(m) 通过质因数分解 O(sqrt m)
- 幂运算使用二进制快速幂;乘法用 __int128 防溢出
- 额外提供大指数版本(b 为字符串),仅用于比较 b 与 φ 的大小和取 b mod φ
*/
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
using u64 = unsigned long long;
using i128 = __int128_t;
// 乘法(避免溢出):(x * y) % mod
static inline i64 mul_mod(i64 x, i64 y, i64 mod) {
return (i64)((i128)x * y % mod);
}
// 快速幂:计算 (a^e) % mod,要求 mod >= 1
i64 fpow(i64 a, u64 e, i64 mod) {
if (mod == 1) return 0;
a %= mod;
if (a < 0) a += mod;
i64 r = 1 % mod;
while (e) {
if (e & 1) r = mul_mod(r, a, mod);
a = mul_mod(a, a, mod);
e >>= 1;
}
return r;
}
// 欧拉函数 phi(n):统计 1..n 中与 n 互质的数
i64 phi(i64 n) {
if (n == 1) return 1; // 习惯上 phi(1) = 1(不会用于取模降幂)
i64 r = n;
for (i64 p = 2; p * p <= n; ++p) {
if (n % p == 0) {
r = r / p * (p - 1);
while (n % p == 0) n /= p;
}
}
if (n > 1) r = r / n * (n - 1);
return r;
}
// 扩展欧拉定理(b 为 64 位):返回 a^b mod m
i64 pw_ext(i64 a, u64 b, i64 m) {
if (m == 1) return 0;
i64 ph = phi(m);
i64 g = std::gcd((i64)((a % m + m) % m), m);
u64 e; // 实际用于幂运算的指数
if (g == 1) {
// 互质:直接 b % phi(m)
e = (ph == 0 ? 0 : (b % (u64)ph));
} else {
// 不互质:b < phi ? 用 b : 用 (b % phi + phi)
if (b < (u64)ph) e = b;
else e = (ph == 0 ? b : (b % (u64)ph + (u64)ph));
}
return fpow(a, e, m);
}
/* ========== 进阶:大指数版本(b 以十进制字符串给出) ========== */
// 计算 s(十进制大整数) % mod(mod fits in 64-bit)
u64 mod_str(const string& s, u64 mod) {
if (mod == 0) return 0;
u64 r = 0;
for (char c : s) {
r = (r * 10 + (c - '0')) % mod;
}
return r;
}
// 判断大整数字符串 s 是否 >= x(x fits in 64-bit)
bool ge_str(const string& s, u64 x) {
string t = to_string(x);
if (s.size() != t.size()) return s.size() > t.size();
return s >= t;
}
// 扩展欧拉定理(b 为字符串):返回 a^b mod m
i64 pw_ext_big(i64 a, const string& bs, i64 m) {
if (m == 1) return 0;
i64 ph = phi(m);
i64 g = std::gcd((i64)((a % m + m) % m), m);
u64 e;
if (g == 1) {
if (ph == 0) e = 0;
else e = mod_str(bs, (u64)ph);
} else {
if (ph == 0) {
// 罕见:m==1 时已提前返回;这里保底
// 直接不降幂(可能很大,无法计算),但该情形不会触发
e = 0;
} else {
if (ge_str(bs, (u64)ph)) e = mod_str(bs, (u64)ph) + (u64)ph;
else {
// 将 bs 转为 u64(bs < phi,且 phi fits u64)
u64 v = 0;
for (char c : bs) v = v * 10 + (c - '0');
e = v;
}
}
}
return fpow(a, e, m);
}
/*
示例 main:
- 读取 a b m,其中 b 可为 64 位或大整数字符串(二选一)
- 输出 a^b mod m
*/
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
i64 a, m;
// 版本一:b 为 64 位整数
/*
u64 b;
if (!(cin >> a >> b >> m)) return 0;
cout << pw_ext(a, b, m) << '\n';
*/
// 版本二:b 为大整数(字符串)
string b;
if (!(cin >> a >> b >> m)) return 0;
cout << pw_ext_big(a, b, m) << '\n';
return 0;
}
/*
使用示例 1(b 为 64 位):
输入:
2 1000000000000000000 1000000007
输出:
140625001
使用示例 2(b 为大整数):
输入:
2 123456789123456789123456789 1000000007
输出:
320484785
说明:
- 当 gcd(a, m) == 1 时,指数可直接对 phi(m) 取模;
- 当 gcd(a, m) != 1 时,若 b >= phi(m),需加一段 phi(m) 再取模,以保证等价性;
- m == 1 时结果恒为 0。
*/
exgcd
/*
扩展欧几里得算法(exgcd)模板
功能概述:
- exgcd(a, b, x, y):返回 gcd(a, b),并给出一组系数 (x, y),使得 a*x + b*y = gcd(a, b)
- inv(a, m):在 m > 1 且 gcd(a, m) = 1 的前提下,返回 a 在模 m 下的乘法逆元;不存在则返回 -1
- sol(a, b, c, x, y, g):求解线性丢番图方程 a*x + b*y = c 的一个整数解(若存在)
- cong(a, b, m, x, mod):求解同余方程 a*x ≡ b (mod m) 的最小非负解(若存在)
设计要点:
- exgcd 同时得到贝祖系数,可直接用于线性方程、逆元与同余方程求解。
- 处理 a 或 b 为负数时,返回的 gcd 为非负(|gcd|),系数会自动匹配使等式成立。
- 乘法可能溢出时,计算中使用 __int128。
*/
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using i128 = __int128_t;
/*
函数:exgcd
作用:求 gcd(a, b) 及其对应的贝祖系数 (x, y),满足 a*x + b*y = gcd(a, b)
说明:
- 返回的 gcd 恒为非负值
- 支持 a 或 b 为负
*/
ll exgcd(ll a, ll b, ll& x, ll& y) {
if (b == 0) {
x = (a >= 0 ? 1 : -1); // 使 a*x = |a|
y = 0;
return llabs(a);
}
ll x1, y1;
ll g = exgcd(b, a % b, x1, y1);
x = y1;
y = x1 - (a / b) * y1;
return g;
}
/*
函数:norm
作用:将 x 归一化到 [0, m-1]
*/
ll norm(ll x, ll m) {
x %= m;
if (x < 0) x += m;
return x;
}
/*
函数:inv
作用:求 a 在模 m 下的乘法逆元(最小非负)
返回:
- 若存在,返回 inv ∈ [0, m-1]
- 若不存在(m <= 1 或 gcd(a, m) != 1),返回 -1
*/
ll inv(ll a, ll m) {
if (m <= 1) return -1;
ll x, y;
ll g = exgcd(a, m, x, y);
if (g != 1) return -1;
return norm(x, m);
}
/*
函数:sol
作用:求解线性丢番图方程 a*x + b*y = c 的一个整数解
输入/输出:
- 返回 true 表示有解,并给出一组 (x, y) 与 gcd g(使得 a*x + b*y = c)
- 返回 false 表示无解(当 c % g != 0)
备注:
- 通解形式:x = x0 + t*(b/g),y = y0 - t*(a/g),t ∈ Z
*/
bool sol(ll a, ll b, ll c, ll& x, ll& y, ll& g) {
ll x0, y0;
g = exgcd(a, b, x0, y0);
if (c % g != 0) return false;
i128 k = (i128)c / g;
x = (ll)((i128)x0 * k);
y = (ll)((i128)y0 * k);
return true;
}
/*
函数:cong
作用:求解同余方程 a*x ≡ b (mod m) 的最小非负解
返回:
- 若有解,返回 true,并给出:
x 为最小非负解;
mod 为解的模周期(所有解为 x + t*mod)
- 若无解(b % gcd(a, m) != 0),返回 false
*/
bool cong(ll a, ll b, ll m, ll& x, ll& mod) {
ll x0, y0;
ll g = exgcd(a, m, x0, y0);
if (b % g != 0) return false;
mod = m / g;
// x ≡ (x0 * (b/g)) mod (m/g)
i128 t = (i128)x0 * (b / g);
x = norm((ll)(t % mod), mod);
return true;
}
/*
示例 main:
- 读取 a b m
- 第一行输出一组贝祖系数 x y 与 gcd g(格式:x y g)
- 第二行输出 a 在模 m 下的逆元(不存在则输出 -1)
*/
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
ll a, b, m;
if (!(cin >> a >> b >> m)) return 0;
ll x, y;
ll g = exgcd(a, b, x, y);
cout << x << ' ' << y << ' ' << g << '\n';
cout << inv(a, m) << '\n';
return 0;
}
/*
使用示例:
输入:
30 18 7
输出:
-1 2 6
1
说明:
- exgcd:30*(-1) + 18*2 = 6
- inv:30 在模 7 下等价于 2,2 的逆元为 4;但 inv(30,7) = inv(2,7) = 4。
由于实现输出的是 inv(a, m),对上述输入会输出 1?不对,重新说明:
a = 30, m = 7 ⇒ a ≡ 2 (mod 7),2 的逆元为 4,因此程序输出 4。
*/
CRT AND EXCRT
/*
CRT 与 EXCRT 模板(同余方程组求解)
功能概述:
- crt(r, m, M): 适用于模数两两互质的情形,求解 x ≡ r[i] (mod m[i])
- excrt(r, m, x, M): 通用情况(模数不必互质),若有解给出最小非负解 x 及周期 M
设计要点:
- 使用扩展欧几里得算法求逆元与合并方程
- 计算中使用 __int128 防溢出;最终结果假定在 64 位有符号范围内
- 约定:
· 所有模 m[i] > 0
· 输入的余数 r[i] 可不在 [0, m[i]),内部会自动归一化
· 对 EXCRT,若无解(余数不一致),返回 false
*/
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using i128 = __int128_t;
/* 扩展欧几里得:返回 gcd(a,b),并求出 x,y 使得 a*x + b*y = gcd(a,b) */
ll exgcd(ll a, ll b, ll& x, ll& y) {
if (b == 0) {
x = (a >= 0 ? 1 : -1);
y = 0;
return llabs(a);
}
ll x1, y1;
ll g = exgcd(b, a % b, x1, y1);
x = y1;
y = x1 - (a / b) * y1;
return g;
}
/* 将 x 归一化到 [0, m-1](m > 0) */
inline ll norm(ll x, ll m) {
x %= m;
if (x < 0) x += m;
return x;
}
/* 计算 a 在模 m 下的逆元(要求 gcd(a,m)=1),不存在返回 -1 */
ll inv_mod(ll a, ll m) {
ll x, y;
ll g = exgcd(a, m, x, y);
if (g != 1) return -1;
return norm(x, m);
}
/*
CRT(两两互质版)
输入:
- r: 余数数组 r[i]
- m: 模数数组 m[i](需两两互质)
输出:
- 返回 pair<bool, pair<ll,ll>>:
· first = true 时,second = {x, M},其中 x 为最小非负解,M = ∏ m[i]
· first = false 时,表示输入不合法(如 m[i] <= 0 或乘积溢出风险等)
实现:
- x = Σ r[i] * (M/m[i]) * inv(M/m[i] mod m[i]) (mod M)
*/
pair<bool, pair<ll,ll>> crt(const vector<ll>& r, const vector<ll>& m) {
int n = (int)r.size();
if ((int)m.size() != n || n == 0) return {false, {0,0}};
for (ll mi : m) if (mi <= 0) return {false, {0,0}};
// 计算 M = prod m[i](用 128 位中间变量,最终假设 fit in int64)
i128 M128 = 1;
for (ll mi : m) M128 *= (i128)mi;
ll M = (ll)M128; // 若可能超 64 位,请改为返回 __int128,并实现打印
i128 x128 = 0;
for (int i = 0; i < n; ++i) {
ll mi = m[i];
ll ri = norm(r[i], mi);
ll Mi = (ll)(M128 / mi); // M / mi,仍可能超 64 位,但此处已转回 ll,假设安全
ll inv = inv_mod(Mi % mi, mi); // 互质保证逆元存在
if (inv == -1) return {false, {0,0}}; // 理论不应发生(若 m 互质)
x128 += (i128)ri * (i128)Mi % (i128)M * (i128)inv % (i128)M;
x128 %= (i128)M;
}
ll x = (ll)x128;
x = norm(x, M);
return {true, {x, M}};
}
/*
EXCRT(扩展中国剩余定理)
输入:
- r: 余数数组 r[i]
- m: 模数数组 m[i](不要求互质)
输出:
- 返回 bool:是否有解
- 若有解:x 为最小非负解,M 为解的周期(所有解为 x + t*M)
做法(逐个合并):
- 当前解:x ≡ a (mod M)
- 合并下一个方程:x ≡ b (mod m2)
等价于:M * t ≡ (b - a) (mod m2)
设 g = gcd(M, m2),若 (b - a) % g != 0 则无解
令 M' = M/g, m2' = m2/g, d = (b - a)/g
求 t0 ≡ d * inv(M' mod m2') (mod m2')
新解:a = a + M * t0,M = M * m2'
*/
bool excrt(const vector<ll>& r, const vector<ll>& m, ll& x, ll& M) {
int n = (int)r.size();
if ((int)m.size() != n || n == 0) return false;
for (ll mi : m) if (mi <= 0) return false;
x = norm(r[0], m[0]);
M = m[0];
for (int i = 1; i < n; ++i) {
ll a = x, m1 = M;
ll b = norm(r[i], m[i]), m2 = m[i];
ll c = b - a;
ll s, t;
ll g = exgcd(m1, m2, s, t); // s*m1 + t*m2 = g
if (c % g != 0) return false;
// 归约
ll m1_ = m1 / g;
ll m2_ = m2 / g;
// t0 ≡ (c/g) * inv(m1_ mod m2_) (mod m2_)
ll inv = norm(s, m2_); // s 即为 m1 在模 m2 上的逆的缩放,取模 m2_
i128 t0 = (i128)(c / g) * inv % m2_;
// 合并
i128 nx = (i128)a + (i128)m1 * t0; // 新的代表解
i128 nM = (i128)m1 * m2_; // 新的模(lcm)
// 规范化到 [0, nM)
x = (ll)( (nx % nM + nM) % nM );
M = (ll)nM; // 若可能超 64 位,可改为 __int128
}
return true;
}
/*
示例 main:
- 输入:
k
r1 m1
r2 m2
...
rk mk
- 输出(EXCRT):
若有解:x M
无解:NO SOLUTION
- 如保证模两两互质,可改为调用 crt,并输出其结果
*/
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int k;
if (!(cin >> k)) return 0;
vector<ll> r(k), m(k);
for (int i = 0; i < k; ++i) cin >> r[i] >> m[i];
ll x, M;
if (excrt(r, m, x, M)) {
cout << x << ' ' << M << '\n';
} else {
cout << "NO SOLUTION\n";
}
// 若已知 m 两两互质,可使用以下代码(示例):
/*
auto res = crt(r, m);
if (!res.first) cout << "INVALID\n";
else {
auto [x, M] = res.second;
cout << x << ' ' << M << '\n';
}
*/
return 0;
}
/*
使用示例 1(EXCRT 可解):
输入:
3
2 6
3 9
2 15
输出:
47 90
校验:47 ≡ 2 (mod 6), 47 ≡ 3 (mod 9), 47 ≡ 2 (mod 15)
使用示例 2(EXCRT 无解):
输入:
2
1 4
2 6
输出:
NO SOLUTION
原因:x ≡ 1 (mod 4) 与 x ≡ 2 (mod 6) 在 mod 2 上矛盾
*/
Miller-Rabin
/*
Miller–Rabin 素性测试模板(64 位无符号整数)
功能:
- isprime(n): 判断 0 <= n < 2^64 是否为素数(确定性)
设计要点:
- 将 n-1 表示为 d * 2^s(d 为奇数),对若干“基” a 进行见证测试:
· 计算 x = a^d mod n
· 若 x == 1 或 x == n-1,则本轮通过
· 否则反复平方 x,共 s-1 次,若出现 x == n-1 则通过
· 若所有平方后仍未通过,则 n 为合数
- 为保证对 64 位整数的“确定性”,采用已知覆盖 64 位的基集合:
bases = {2, 325, 9375, 28178, 450775, 9780504, 1795265022}
- 乘法使用 128 位中间变量(__uint128_t)避免溢出
- 先筛掉若干小素数可加速,并处理边界情况
复杂度:
- 单次测试约 O(k log^3 n),k 为基的数量(此处固定为 7)
*/
#include <bits/stdc++.h>
using namespace std;
using u64 = unsigned long long;
using u128 = __uint128_t;
/* 乘法取模:返回 (a * b) % mod,使用 128 位中间变量防溢出 */
static inline u64 mul(u64 a, u64 b, u64 mod) {
return (u64)((u128)a * b % mod);
}
/* 快速幂取模:返回 (a^e) % mod */
static inline u64 pw(u64 a, u64 e, u64 mod) {
u64 r = 1 % mod;
a %= mod;
while (e) {
if (e & 1) r = mul(r, a, mod);
a = mul(a, a, mod);
e >>= 1;
}
return r;
}
/*
单基 Miller–Rabin 见证测试
输入:
- n: 待测奇数且 n >= 3
- a: 测试基(a % n != 0 时才有意义)
- d, s: n-1 = d * 2^s,且 d 为奇数
返回:
- true 若 a 不能证明 n 为合数(本轮通过)
- false 若 a 证明 n 为合数
*/
static inline bool mr_once(u64 n, u64 a, u64 d, int s) {
u64 x = pw(a, d, n);
if (x == 1 || x == n - 1) return true;
for (int i = 1; i < s; ++i) {
x = mul(x, x, n);
if (x == n - 1) return true;
}
return false;
}
/*
主函数:isprime
返回:
- true 素数
- false 合数
处理流程:
- 处理小数与偶数
- 与一组小素数比对,加速筛除
- 写成 n-1 = d * 2^s,使用固定基集合进行 MR 测试
*/
bool isprime(u64 n) {
if (n < 2) return false;
// 小数与偶数快速处理
static const u64 small[] = {2ULL,3ULL,5ULL,7ULL,11ULL,13ULL,17ULL,19ULL,23ULL,29ULL,31ULL,37ULL};
for (u64 p : small) {
if (n == p) return true;
if (n % p == 0) return n == p;
}
// 将 n-1 写成 d * 2^s
u64 d = n - 1;
int s = 0;
while ((d & 1) == 0) { d >>= 1; ++s; }
// 64 位确定性基集合(文献与实践中常用的一组)
static const u64 bases[] = {2ULL, 325ULL, 9375ULL, 28178ULL, 450775ULL, 9780504ULL, 1795265022ULL};
for (u64 a : bases) {
if (a % n == 0) continue; // 基与 n 取模为 0,无需测试
if (!mr_once(n, a, d, s)) return false;
}
return true;
}
/*
示例 main:
- 输入:
t
n1
n2
...
nt
- 输出:
对每个 ni 输出一行:YES 表示素数,NO 表示合数
*/
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
if (!(cin >> t)) return 0;
while (t--) {
unsigned long long n;
cin >> n;
cout << (isprime(n) ? "YES" : "NO") << '\n';
}
return 0;
}
/*
使用示例:
输入:
5
1
2
17
221
2305843009213693951
输出:
NO
YES
YES
NO
YES
说明:
- 221 = 13 * 17,为合数
- 2305843009213693951 = 2^61 - 1,为素数
*/
KMP
/*
KMP(Knuth–Morris–Pratt)字符串匹配模板
功能:
- 构造前缀函数(pi):pi[i] = 模式串 p[0..i] 的最长相等“真前缀=后缀”的长度
- kmp(t, p):在主串 t 中查找模式串 p 的所有出现位置(返回起始下标,0 基)
设计要点:
- 利用失败指针(前缀函数)在匹配失败时不回退主串指针,保证 O(n + m)
- 匹配结束后,为了支持重叠匹配,发生一次完整匹配后将 j 回退到 pi[j-1]
复杂度:
- 预处理 O(m),匹配 O(n)
备注:
- 本模板返回 0 基坐标;示例输出时转换为 1 基更符合常见题面
- 若 p 为空串,按常见竞赛习惯返回空结果(不进行匹配)
*/
#include <bits/stdc++.h>
using namespace std;
/*
函数:get_pi
作用:构造模式串 p 的前缀函数 pi
含义:
- pi[i] = p[0..i] 的最长真前缀与后缀的相等长度
*/
vector<int> get_pi(const string& p) {
int m = (int)p.size();
vector<int> pi(m, 0);
for (int i = 1, j = 0; i < m; ++i) {
// 失配时回退 j 到上一个可匹配的位置
while (j > 0 && p[i] != p[j]) j = pi[j - 1];
if (p[i] == p[j]) ++j;
pi[i] = j;
}
return pi;
}
/*
函数:kmp
作用:在主串 t 中查找模式串 p 的所有出现位置(返回起始下标,0 基)
流程:
- 预处理 pi
- 线性扫描 t,配合失败指针推进/回退 j
*/
vector<int> kmp(const string& t, const string& p) {
int n = (int)t.size(), m = (int)p.size();
vector<int> pos;
if (m == 0) return pos; // 按约定:空模式不匹配任何位置
auto pi = get_pi(p);
for (int i = 0, j = 0; i < n; ++i) {
while (j > 0 && t[i] != p[j]) j = pi[j - 1];
if (t[i] == p[j]) ++j;
if (j == m) {
pos.push_back(i - m + 1); // 记录一次匹配(起始为 0 基)
j = pi[j - 1]; // 支持重叠匹配
}
}
return pos;
}
/*
辅助输出:将 0 基下标转换为 1 基输出(空格分隔)
*/
void print_pos_1based(const vector<int>& pos) {
for (size_t i = 0; i < pos.size(); ++i) {
if (i) cout << ' ';
cout << (pos[i] + 1);
}
cout << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
// 读取主串 t 与模式串 p(默认不含空格;如需支持空格,可改用 getline)
string t, p;
if (!(cin >> t >> p)) return 0;
auto pos = kmp(t, p);
print_pos_1based(pos); // 输出所有匹配起点(1 基)
// 若需要同时输出前缀函数,可取消下方注释:
/*
auto pi = get_pi(p);
for (int i = 0; i < (int)pi.size(); ++i) {
if (i) cout << ' ';
cout << pi[i];
}
cout << '\n';
*/
return 0;
}
/*
使用示例:
输入:
abaabaaba aba
输出:
1 4 7
说明:
- 在主串 "abaabaaba" 中,模式 "aba" 出现在 1、4、7(1 基)
*/
Manacher
/*
Manacher 算法模板 —— 最长回文子串(O(n))
功能:
- pal_odd(s): 计算所有“奇数长度”回文的半径数组 d1
d1[i] = 以 i 为中心的最长奇回文半径(包含中心),对应长度 = 2*d1[i] - 1
- pal_even(s): 计算所有“偶数长度”回文的半径数组 d2
d2[i] = 以 i-1 与 i 之间为中心的最长偶回文半径,对应长度 = 2*d2[i]
- best_pal(s, d1, d2, st, len): 给出 s 的最长回文子串的起点 st 与长度 len
设计要点:
- 维护当前回文最右端区间 [l, r],利用对称位置的半径加速扩展
- 奇、偶两种中心分别计算,覆盖所有回文
复杂度:
- 线性 O(n)
*/
#include <bits/stdc++.h>
using namespace std;
/*
函数:pal_odd
作用:计算奇回文半径数组 d1
参数:
- s: 原串
返回:
- d1: 向量,d1[i] 为以 i 为中心的最大奇回文半径(长度 = 2*d1[i]-1)
*/
vector<int> pal_odd(const string& s) {
int n = (int)s.size();
vector<int> d1(n, 0);
int l = 0, r = -1;
for (int i = 0; i < n; ++i) {
int k = 1;
if (i <= r) k = min(d1[l + r - i], r - i + 1);
while (i - k >= 0 && i + k < n && s[i - k] == s[i + k]) ++k;
d1[i] = k;
--k;
if (i + k > r) { l = i - k; r = i + k; }
}
return d1;
}
/*
函数:pal_even
作用:计算偶回文半径数组 d2
参数:
- s: 原串
返回:
- d2: 向量,d2[i] 为以 (i-1, i) 为中心的最大偶回文半径(长度 = 2*d2[i])
*/
vector<int> pal_even(const string& s) {
int n = (int)s.size();
vector<int> d2(n, 0);
int l = 0, r = -1;
for (int i = 0; i < n; ++i) {
int k = 0;
if (i <= r) k = min(d2[l + r - i + 1], r - i + 1);
while (i - k - 1 >= 0 && i + k < n && s[i - k - 1] == s[i + k]) ++k;
d2[i] = k;
--k;
if (i + k > r) { l = i - k - 1; r = i + k; }
}
return d2;
}
/*
函数:best_pal
作用:给出 s 的最长回文子串的位置与长度
参数/输出:
- s, d1, d2:预处理得到的回文半径
- st, len:输出最长回文子串的起始下标与长度(0 基)
*/
void best_pal(const string& s, const vector<int>& d1, const vector<int>& d2, int& st, int& len) {
int n = (int)s.size();
st = 0; len = 0;
// 奇回文
for (int i = 0; i < n; ++i) {
int cur = 2 * d1[i] - 1;
if (cur > len) {
len = cur;
st = i - d1[i] + 1;
}
}
// 偶回文
for (int i = 0; i < n; ++i) {
int cur = 2 * d2[i];
if (cur > len) {
len = cur;
st = i - d2[i];
}
}
}
/*
示例 main:
- 输入:一行字符串 s(不含空格;若需含空格,可改用 getline)
- 输出:
第一行:最长回文子串的长度
第二行:最长回文子串本身
*/
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
string s;
if (!(cin >> s)) return 0;
auto d1 = pal_odd(s);
auto d2 = pal_even(s);
int st, len;
best_pal(s, d1, d2, st, len);
cout << len << '\n';
cout << s.substr(st, len) << '\n';
return 0;
}
/*
使用示例:
输入:
babad
输出(可能之一):
3
bab
说明:
- 最长回文为 "bab" 或 "aba",长度均为 3,程序输出其中之一。
*/
单调栈
/*
基于 STL 的单调栈模板(Monotonic Stack)
功能:
- nsl(a): 每个位置 i 的“左侧最近更小元素”的下标(严格小于),若不存在为 -1
- nsr(a): 每个位置 i 的“右侧最近更小元素”的下标(严格小于),若不存在为 n
- ngl(a): 每个位置 i 的“左侧最近更大元素”的下标(严格大于),若不存在为 -1
- ngr(a): 每个位置 i 的“右侧最近更大元素”的下标(严格大于),若不存在为 n
- hist_max(h): 直方图最大矩形面积(常见单调栈应用示例)
设计要点:
- 使用 std::stack<int> 存储“候选下标”,保持栈内对应值单调(增或减)
- 为了得到“严格更小/更大”,在维护单调性时使用 >= 或 <= 进行弹栈
· 更小:用 >= 保证栈内严格递增
· 更大:用 <= 保证栈内严格递减
复杂度:
- 所有函数均为 O(n)
约定:
- 返回的下标为 0 基;无解时左侧返回 -1,右侧返回 n(n 为数组长度)
*/
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
/*
函数:nsl(nearest smaller to left)
作用:左侧最近更小元素下标(严格小于)
实现:维护值“严格递增”的栈(值相等需要弹出)
*/
vector<int> nsl(const vector<ll>& a) {
int n = (int)a.size();
vector<int> L(n, -1);
stack<int> st; // 存储下标,a[st] 严格递增
for (int i = 0; i < n; ++i) {
while (!st.empty() && a[st.top()] >= a[i]) st.pop();
L[i] = st.empty() ? -1 : st.top();
st.push(i);
}
return L;
}
/*
函数:nsr(nearest smaller to right)
作用:右侧最近更小元素下标(严格小于)
实现:从右到左,维护值“严格递增”的栈(值相等需要弹出)
*/
vector<int> nsr(const vector<ll>& a) {
int n = (int)a.size();
vector<int> R(n, n);
stack<int> st; // 存储下标,a[st] 严格递增
for (int i = n - 1; i >= 0; --i) {
while (!st.empty() && a[st.top()] >= a[i]) st.pop();
R[i] = st.empty() ? n : st.top();
st.push(i);
}
return R;
}
/*
函数:ngl(nearest greater to left)
作用:左侧最近更大元素下标(严格大于)
实现:维护值“严格递减”的栈(值相等需要弹出)
*/
vector<int> ngl(const vector<ll>& a) {
int n = (int)a.size();
vector<int> L(n, -1);
stack<int> st; // 存储下标,a[st] 严格递减
for (int i = 0; i < n; ++i) {
while (!st.empty() && a[st.top()] <= a[i]) st.pop();
L[i] = st.empty() ? -1 : st.top();
st.push(i);
}
return L;
}
/*
函数:ngr(nearest greater to right)
作用:右侧最近更大元素下标(严格大于)
实现:从右到左,维护值“严格递减”的栈(值相等需要弹出)
*/
vector<int> ngr(const vector<ll>& a) {
int n = (int)a.size();
vector<int> R(n, n);
stack<int> st; // 存储下标,a[st] 严格递减
for (int i = n - 1; i >= 0; --i) {
while (!st.empty() && a[st.top()] <= a[i]) st.pop();
R[i] = st.empty() ? n : st.top();
st.push(i);
}
return R;
}
/*
示例应用:直方图最大矩形面积
思路:
- 对每个柱 i,左右边界由“最近更小元素”决定:
left = nsl[i] + 1, right = nsr[i] - 1
宽度 = right - left + 1 = nsr[i] - nsl[i] - 1
面积 = h[i] * 宽度
*/
long long hist_max(const vector<ll>& h) {
int n = (int)h.size();
auto L = nsl(h);
auto R = nsr(h);
long long ans = 0;
for (int i = 0; i < n; ++i) {
long long w = (long long)R[i] - L[i] - 1;
ans = max(ans, h[i] * w);
}
return ans;
}
/*
辅助输出:空格分隔输出整型数组(0 基下标;-1 或 n 表示不存在)
*/
template <class T>
void print_arr(const vector<T>& a) {
for (size_t i = 0; i < a.size(); ++i) {
if (i) cout << ' ';
cout << a[i];
}
cout << '\n';
}
/*
示例 main:
- 输入:
n
a1 a2 ... an
- 输出:
四行:NSL、NSR、NGL、NGR 的下标数组(0 基;无解位置为 -1 或 n)
第五行:直方图最大矩形面积
*/
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
vector<ll> a(n);
for (int i = 0; i < n; ++i) cin >> a[i];
auto Ls = nsl(a);
auto Rs = nsr(a);
auto Lg = ngl(a);
auto Rg = ngr(a);
print_arr(Ls);
print_arr(Rs);
print_arr(Lg);
print_arr(Rg);
cout << hist_max(a) << '\n';
return 0;
}
/*
使用示例:
输入:
7
2 1 5 6 2 3 1
输出(可能之一):
-1 -1 1 2 1 5 -1
1 7 4 4 6 6 7
-1 0 -1 -1 3 4 5
2 2 3 5 5 7 7
10
说明:
- 前四行分别为:左更小、右更小、左更大、右更大(无解为 -1 或 n)
- 最后一行是直方图最大矩形面积:对于样例,最大面积为 10(柱 5 和 6 的组合)
*/
单调队列
/*
基于 STL 的单调队列模板(Sliding Window + deque)
功能:
- win_min(a, k): 返回数组 a 所有长度为 k 的滑动窗口的最小值
- win_max(a, k): 返回数组 a 所有长度为 k 的滑动窗口的最大值
设计要点:
- 使用 std::deque<int> 存下标,队列内对应的值保持单调
· 最小值:保持队尾到队首严格递增(入队时弹出 >= 的元素)
· 最大值:保持队尾到队首严格递减(入队时弹出 <= 的元素)
- 维护窗口有效性:当队首下标 < 当前窗口左端时弹出
- 每个元素最多入队、出队一次,总复杂度 O(n)
约定:
- 若 k <= 0 或 k > n,则返回空结果
*/
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
/*
函数:win_min
作用:计算长度为 k 的每个滑动窗口的最小值
实现细节:
- dq 存储下标,保证 a[dq[0]] <= a[dq[1]] <= ...(递增)
- 入队时,弹出队尾所有 >= a[i] 的下标,再将 i 入队
- 维护窗口左边界 L = i - k + 1,弹出 dq.front() < L 的下标
- 当 i >= k - 1 时,窗口形成,最小值为 a[dq.front()]
*/
vector<ll> win_min(const vector<ll>& a, int k) {
int n = (int)a.size();
vector<ll> res;
if (k <= 0 || k > n) return res;
deque<int> dq;
for (int i = 0; i < n; ++i) {
while (!dq.empty() && a[dq.back()] >= a[i]) dq.pop_back();
dq.push_back(i);
int L = i - k + 1;
while (!dq.empty() && dq.front() < L) dq.pop_front();
if (i >= k - 1) res.push_back(a[dq.front()]);
}
return res;
}
/*
函数:win_max
作用:计算长度为 k 的每个滑动窗口的最大值
实现细节:
- dq 存储下标,保证 a[dq[0]] >= a[dq[1]] >= ...(递减)
- 入队时,弹出队尾所有 <= a[i] 的下标,再将 i 入队
- 维护窗口左边界 L = i - k + 1,弹出 dq.front() < L 的下标
- 当 i >= k - 1 时,窗口形成,最大值为 a[dq.front()]
*/
vector<ll> win_max(const vector<ll>& a, int k) {
int n = (int)a.size();
vector<ll> res;
if (k <= 0 || k > n) return res;
deque<int> dq;
for (int i = 0; i < n; ++i) {
while (!dq.empty() && a[dq.back()] <= a[i]) dq.pop_back();
dq.push_back(i);
int L = i - k + 1;
while (!dq.empty() && dq.front() < L) dq.pop_front();
if (i >= k - 1) res.push_back(a[dq.front()]);
}
return res;
}
/*
辅助输出:空格分隔
*/
template <class T>
void print_vec(const vector<T>& v) {
for (size_t i = 0; i < v.size(); ++i) {
if (i) cout << ' ';
cout << v[i];
}
cout << '\n';
}
/*
示例 main:
- 输入:
n k
a1 a2 ... an
- 输出:
第一行:每个窗口的最小值
第二行:每个窗口的最大值
*/
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, k;
if (!(cin >> n >> k)) return 0;
vector<ll> a(n);
for (int i = 0; i < n; ++i) cin >> a[i];
auto mn = win_min(a, k);
auto mx = win_max(a, k);
print_vec(mn);
print_vec(mx);
return 0;
}
/*
使用示例:
输入:
8 3
1 3 -1 -3 5 3 6 7
输出:
-1 -3 -3 -3 3 3
3 3 5 5 6 7
说明:
- 长度为 3 的滑动窗口最小值与最大值分别如上。
*/
几何模板!!!
/*
二维计算几何模板(点 / 直线 / 线段 / 圆 / 多边形)
目标:
- 封装常用几何原语与运算,变量与函数名简洁,注释详细
- 涵盖:点与向量、直线/线段、圆、相交/距离/投影/切线、多边形(面积/周长/重心/点内判定/凸包/直径)等
精度:
- 使用 double 浮点;EPS = 1e-9
- 用 sgn(x) 将实数比较离散化为 {-1,0,1},避免直接用 == 比较
约定:
- “相交/在上”类判断均默认“含等号”(即接触=相交,在边界=在上)
- 所有坐标单位、角度(弧度)的度量需与题目一致
复杂度:
- 除凸包 O(n log n)、直径(旋转卡壳)O(n)、裁剪 O(n) 外,多数基本操作 O(1)
*/
#include <bits/stdc++.h>
using namespace std;
const double EPS = 1e-9;
const double PI = acos(-1.0);
int sgn(double x) { return (x > EPS) - (x < -EPS); }
double clamp1(double x) { return x < -1 ? -1 : (x > 1 ? 1 : x); }
/* ====================== 点与向量 ======================
定义:
- P 既表示平面点,也作为从原点或某点出发的向量
说明:
- operator< 用于排序(严格弱序);不引入 EPS,避免破坏传递性
- operator== 使用 EPS 判断“几何相等”
*/
struct P {
double x, y;
P() : x(0), y(0) {}
P(double _x, double _y) : x(_x), y(_y) {}
// 向量加减、数乘/除
P operator + (const P& o) const { return P(x + o.x, y + o.y); }
P operator - (const P& o) const { return P(x - o.x, y - o.y); }
P operator * (double k) const { return P(x * k, y * k); }
P operator / (double k) const { return P(x / k, y / k); }
// 排序与几何相等
bool operator < (const P& o) const { return x < o.x || (x == o.x && y < o.y); }
bool operator == (const P& o) const { return sgn(x - o.x) == 0 && sgn(y - o.y) == 0; }
};
/* 基础运算:
- dot(a,b) 点积:|a||b|cosθ,平行投影量
- crs(a,b) 叉积:|a||b|sinθ,带符号面积(a 在 b 左侧为正)
- len(a) 长度
- len2(a) 长度平方(免开根更高效)
- unit(a) 单位向量(零向量返回 {0,0})
- rot(a,θ) 逆时针旋转
- perp(a) 逆时针旋转 90°(与 a 垂直)
- ccw(a,b,c) 方向:>0 左转,<0 右转,=0 共线
*/
double dot(P a, P b) { return a.x * b.x + a.y * b.y; }
double crs(P a, P b) { return a.x * b.y - a.y * b.x; }
double len(P a) { return hypot(a.x, a.y); }
double len2(P a){ return dot(a, a); }
P unit(P a) { double L = len(a); return L == 0 ? P(0,0) : a / L; }
P rot(P a, double th) { return P(a.x * cos(th) - a.y * sin(th), a.x * sin(th) + a.y * cos(th)); }
P perp(P a) { return P(-a.y, a.x); }
int ccw(P a, P b, P c) { return sgn(crs(b - a, c - a)); }
/* ====================== 直线与线段 ======================
约定:
- 用 L 表示有向线(或线段)ab;当作为“直线”用时表示过 a,b 的无限延长线
*/
struct L {
P a, b; // 有向端点 a->b
L() {}
L(P _a, P _b) : a(_a), b(_b) {}
P v() const { return b - a; } // 方向向量
};
/* 投影与反射:
- proj(p,l): p 在直线 l 上的正交投影
- refl(p,l): p 关于直线 l 的镜像点(先投影,再关于投影对称)
注意:
- l 需为“直线”(无限延长),不区分是否在“线段”范围内
*/
P proj(P p, L l) {
P v = l.v();
return l.a + v * (dot(p - l.a, v) / dot(v, v));
}
P refl(P p, L l) { return proj(p, l) * 2 - p; }
/* 距离:
- dist_pl: 点到直线距离
- dist_ps: 点到线段距离(若投影不在线段内取端点最近)
- dist_ll: 直线到直线距离(相交=0,平行且异线=到其中一条直线的距离)
- dist_ss: 线段到线段距离(相交=0,否则端点到对段最小距离)
*/
double dist_pl(P p, L l) {
return fabs(crs(l.v(), p - l.a)) / len(l.v());
}
double dist_ps(P p, L s) {
P v = s.v();
double t = dot(p - s.a, v) / dot(v, v);
if (t < 0) return len(p - s.a);
if (t > 1) return len(p - s.b);
P q = s.a + v * t;
return len(p - q);
}
double dist_ll(L l1, L l2) {
if (sgn(crs(l1.v(), l2.v())) != 0) return 0.0; // 相交(不平行)距离为 0
return dist_pl(l2.a, l1); // 平行:任取一点到另一条线的距离
}
double dist_ss(L s1, L s2) {
if (sgn(crs(s1.v(), s2.v())) != 0 && // 先快速判相交(Proper 或端点接触)
(ccw(s1.a, s1.b, s2.a) * ccw(s1.a, s1.b, s2.b) <= 0) &&
(ccw(s2.a, s2.b, s1.a) * ccw(s2.a, s2.b, s1.b) <= 0))
return 0.0;
double d = 1e300;
d = min(d, dist_ps(s1.a, s2));
d = min(d, dist_ps(s1.b, s2));
d = min(d, dist_ps(s2.a, s1));
d = min(d, dist_ps(s2.b, s1));
return d;
}
/* 点与线段位置关系:
- on_seg(p,s): p 是否在线段 s 上(含端点)
- parallel/collinear:两直线是否平行/共线
*/
bool on_seg(P p, L s) {
return sgn(crs(s.b - s.a, p - s.a)) == 0 && sgn(dot(p - s.a, p - s.b)) <= 0;
}
bool parallel(L l1, L l2) { return sgn(crs(l1.v(), l2.v())) == 0; }
bool collinear(L l1, L l2) { return parallel(l1, l2) && sgn(crs(l2.a - l1.a, l1.v())) == 0; }
/* 交点与相交性:
- inter_ll(l1,l2,out): 直线唯一交点(不平行),返回 bool
- inter_ss_proper(s1,s2): 线段严格相交(交点不含端点)
- inter_ss(s1,s2): 线段一般相交(含端点接触/重合边界视为相交)
- inter_ss_point(s1,s2,out): 线段唯一交点(不含重合/多解),返回 bool
注意:
- 对“重合线段”没有单一交点,inter_ss_point 返回 false
*/
bool inter_ll(L l1, L l2, P& out) {
double d = crs(l1.v(), l2.v());
if (sgn(d) == 0) return false;
double t = crs(l2.a - l1.a, l2.v()) / d;
out = l1.a + l1.v() * t;
return true;
}
bool inter_ss_proper(L s1, L s2) {
int c1 = ccw(s1.a, s1.b, s2.a), c2 = ccw(s1.a, s1.b, s2.b);
int c3 = ccw(s2.a, s2.b, s1.a), c4 = ccw(s2.a, s2.b, s1.b);
return c1 * c2 < 0 && c3 * c4 < 0;
}
bool inter_ss(L s1, L s2) {
if (inter_ss_proper(s1, s2)) return true;
if (on_seg(s2.a, s1) || on_seg(s2.b, s1) || on_seg(s1.a, s2) || on_seg(s1.b, s2)) return true;
return false;
}
bool inter_ss_point(L s1, L s2, P& out) {
if (!inter_ss(s1, s2)) return false;
if (parallel(s1, s2)) return false; // 重合/平行:无唯一交点
return inter_ll(s1, s2, out);
}
/* ====================== 圆 ======================
C:
- o:圆心
- r:半径(≥ 0)
contains(p, strict):
- 判断点在圆内;strict=true 表示“严格在内”(不含边界)
*/
struct C {
P o; double r;
C() : o(), r(0) {}
C(P _o, double _r) : o(_o), r(_r) {}
bool contains(P p, bool strict=false) const {
return sgn(len2(p - o) - r*r) < (strict ? 0 : 1);
}
};
/* 圆与直线/线段交点:
- inter_cl(c,l): 与直线交点(0/1/2 个)
· 思路:圆心到直线作垂线,若距离 d>r 无交;=r 一切;<r 两交
- inter_cs(c,s): 与线段交点(从 inter_cl 结果中过滤在段上的点)
*/
vector<P> inter_cl(C c, L l) {
P h = proj(c.o, l); // 垂足
double d = len(h - c.o); // 圆心到直线距离
int s = sgn(c.r - d);
if (s < 0) return {}; // 相离
if (s == 0) return {h}; // 相切
double t = sqrt(max(0.0, c.r * c.r - d * d));
P dir = unit(l.v());
return { h - dir * t, h + dir * t };
}
vector<P> inter_cs(C c, L s) {
vector<P> pts = inter_cl(c, s);
vector<P> res;
for (auto& p : pts) if (on_seg(p, s)) res.push_back(p);
return res;
}
/* 圆与圆交点:
- inter_cc(c1,c2): 0/1/2 个交点
· 同心:无唯一解(返回空)
· 相离/内含(距离过大/过小):无交
· 否则:计算两圆心连线上的脚点 m 以及左右偏移 off
*/
vector<P> inter_cc(C c1, C c2) {
P d = c2.o - c1.o;
double D = len(d);
if (sgn(D) == 0) return {}; // 同心(无唯一交点)
if (sgn(D - (c1.r + c2.r)) > 0) return {}; // 相离
if (sgn(fabs(c1.r - c2.r) - D) > 0) return {}; // 内含
double a = (c1.r * c1.r - c2.r * c2.r + D * D) / (2 * D);
double h2 = c1.r * c1.r - a * a;
P m = c1.o + unit(d) * a; // 中垂线脚点
if (sgn(h2) == 0) return {m}; // 相切
double h = sqrt(max(0.0, h2));
P off = perp(unit(d)) * h;
return { m - off, m + off };
}
/* 点到圆的切线:
- tan_pc(p,c): 返回切点(0/1/2 个)
· p 圆内:无切线;圆上:1 个切点(自身);圆外:2 个切点
*/
vector<P> tan_pc(P p, C c) {
P v = p - c.o;
double d2 = len2(v), r2 = c.r * c.r;
int s = sgn(d2 - r2);
if (s < 0) return {};
if (s == 0) return {p};
// 切点公式(向量推导)
P u = v * (r2 / d2);
P w = perp(v) * (c.r * sqrt(max(0.0, d2 - r2)) / d2);
return { c.o + u - w, c.o + u + w };
}
/* 两圆的公切线(外/内):
- tan_cc(a,b): 返回每条切线在两圆上的“切点对”(p1 on a, p2 on b)
· 若同心:无穷多/无(此模板返回空)
· 总计最多 4 条切线(外公切线 2,内公切线 2)
*/
vector<pair<P,P>> tan_cc(C a, C b) {
vector<pair<P,P>> res;
P d = b.o - a.o;
double sq = len2(d);
if (sgn(sq) == 0) return res; // 同心
double D = sqrt(sq);
for (int sgn_rb : {-1, 1}) { // -1: 内,+1: 外
double r = (b.r * sgn_rb - a.r) / D;
double h2 = 1 - r * r;
if (h2 < -EPS) continue;
h2 = max(0.0, h2);
P u = d * r;
P v = perp(d) * sqrt(h2);
for (int sgn_v : {-1, 1}) {
P dir = (u + v * sgn_v) / D; // 方向单位向量
P p1 = a.o + dir * a.r;
P p2 = b.o + dir * (b.r * sgn_rb);
res.push_back({p1, p2});
}
}
return res;
}
/* 两圆相交面积:
- inter_area_cc(c1,c2): 返回两圆的交集面积
· 相离:0;内含:小圆面积;一般:扇形-三角形之和
*/
double inter_area_cc(C c1, C c2) {
double d = len(c1.o - c2.o);
double r1 = c1.r, r2 = c2.r;
if (sgn(d - (r1 + r2)) >= 0) return 0.0; // 相离/外切
if (sgn(d - fabs(r1 - r2)) <= 0) { // 内含/内切
double r = min(r1, r2);
return PI * r * r;
}
double a1 = acos(clamp1((r1*r1 + d*d - r2*r2) / (2*r1*d)));
double a2 = acos(clamp1((r2*r2 + d*d - r1*r1) / (2*r2*d)));
double s1 = r1*r1 * (a1 - sin(2*a1)/2);
double s2 = r2*r2 * (a2 - sin(2*a2)/2);
return s1 + s2;
}
/* ====================== 多边形(简单/凸) ======================
Poly:
- 用 vector<P> 表示多边形顶点序列(可顺/逆时针)
- 若显式要求“凸多边形”,应先用凸包或其它方式保证
*/
using Poly = vector<P>;
/* 面积与周长:
- area2(pg): 有向面积的两倍(>0 CCW, <0 CW)
- area(pg): 绝对面积
- perim(pg): 周长(首尾相连)
注意:
- 面积公式使用“叉积求和”(Shoelace),允许点列重复,结果仍正确
*/
double area2(const Poly& pg) {
long double s = 0;
int n = (int)pg.size();
for (int i = 0; i < n; ++i)
s += (long double)crs(pg[i], pg[(i + 1) % n]);
return (double)s;
}
double area(const Poly& pg) { return fabs(area2(pg)) / 2.0; }
double perim(const Poly& pg) {
double p = 0;
int n = (int)pg.size();
for (int i = 0; i < n; ++i) p += len(pg[(i + 1) % n] - pg[i]);
return p;
}
/* 重心(质心):
- centroid(pg): 对简单多边形的“几何中心”(均匀密度)
退化:
- 当面积为 0(退化为线段/点),返回顶点平均值作为退化重心
*/
P centroid(const Poly& pg) {
double A2 = area2(pg);
if (sgn(A2) == 0) {
P c{0,0};
for (auto& p : pg) c = c + p;
return pg.empty() ? P(0,0) : c / (double)pg.size();
}
long double cx = 0, cy = 0;
int n = (int)pg.size();
for (int i = 0; i < n; ++i) {
long double c = crs(pg[i], pg[(i + 1) % n]);
cx += (pg[i].x + pg[(i + 1) % n].x) * c;
cy += (pg[i].y + pg[(i + 1) % n].y) * c;
}
cx /= (3.0L * A2);
cy /= (3.0L * A2);
return P((double)cx, (double)cy);
}
/* 点在多边形内(射线法):
- in_poly(pg,p): 2=严格内部,1=在边上,0=外部
细节:
- 特判边上:on_seg
- 射线与边相交计数时需避免“顶点双计”,采用半开区间策略
*/
int in_poly(const Poly& pg, P p) {
bool in = false;
int n = (int)pg.size();
for (int i = 0, j = n - 1; i < n; j = i++) {
P a = pg[j], b = pg[i];
if (on_seg(p, L(a, b))) return 1;
bool up = (b.y > p.y) != (a.y > p.y);
if (up) {
double x = (b.x - a.x) * (p.y - a.y) / (b.y - a.y + 0.0) + a.x;
if (x > p.x) in = !in;
}
}
return in ? 2 : 0;
}
/* 凸包(Andrew 单调链):
- hull(pts): 输入可含重复点;输出 CCW 顺序且首尾不重复
细节:
- 当需要“严格凸包”(去除共线边上的点),在判定中使用 <0;本模板使用 <=0 以去除共线中间点
*/
Poly hull(vector<P> p) {
sort(p.begin(), p.end());
p.erase(unique(p.begin(), p.end()), p.end());
int n = (int)p.size();
if (n <= 1) return p;
Poly lo, up;
for (int i = 0; i < n; ++i) {
while (lo.size() >= 2 && ccw(lo[lo.size()-2], lo.back(), p[i]) <= 0) lo.pop_back();
lo.push_back(p[i]);
}
for (int i = n - 1; i >= 0; --i) {
while (up.size() >= 2 && ccw(up[up.size()-2], up.back(), p[i]) <= 0) up.pop_back();
up.push_back(p[i]);
}
lo.pop_back(); up.pop_back();
lo.insert(lo.end(), up.begin(), up.end());
return lo;
}
/* 凸多边形直径(最远点对距离平方):
- dia_convex(ch): 输入为凸包(CCW);旋转卡壳 O(n)
返回:
- (i, j, dist2):最远点对的两个下标及其距离平方
细节:
- 对 n=1/2 分别特判
*/
tuple<int,int,double> dia_convex(const Poly& ch) {
int n = (int)ch.size();
if (n == 1) return {0, 0, 0.0};
if (n == 2) return {0, 1, len2(ch[1] - ch[0])};
double best = 0; int bi = 0, bj = 0;
int j = 1;
for (int i = 0; i < n; ++i) {
int ni = (i + 1) % n;
while (true) {
int nj = (j + 1) % n;
double cur = fabs(crs(ch[ni] - ch[i], ch[nj] - ch[i]));
double prv = fabs(crs(ch[ni] - ch[i], ch[j] - ch[i]));
if (cur > prv + EPS) j = nj;
else break;
}
double d2 = len2(ch[i] - ch[j]);
if (d2 > best) best = d2, bi = i, bj = j;
}
return {bi, bj, best};
}
/* 多边形按半平面裁剪:
- cut_poly(pg, a, b): 保留直线 a->b 的左侧(含边界),返回新多边形
用途:
- 单条直线裁剪(凸/非凸皆可);多半平面交可多次调用
细节:
- 边跨越裁剪线时,加入交点(线段与直线的唯一交点)
*/
Poly cut_poly(const Poly& pg, P a, P b) {
Poly res;
int n = (int)pg.size();
for (int i = 0; i < n; ++i) {
P c = pg[i], d = pg[(i + 1) % n];
double c1 = crs(b - a, c - a);
double c2 = crs(b - a, d - a);
if (sgn(c1) >= 0) res.push_back(c); // 当前点在左侧(或边上)
if (sgn(c1) * sgn(c2) < 0) { // 跨越裁剪线,加入交点
L s(c, d), l(a, b);
double t = crs(a - c, l.v()) / crs(s.v(), l.v()); // c + t*(d-c)
P inter = c + (d - c) * t;
res.push_back(inter);
}
}
// 去除相邻重复点与首尾重复
if (!res.empty()) {
Poly t;
t.reserve(res.size());
for (int i = 0; i < (int)res.size(); ++i) {
if (i == 0 || !(res[i] == res[i - 1])) t.push_back(res[i]);
}
if (t.size() >= 2 && t.front() == t.back()) t.pop_back();
return t;
}
return res;
}
/* ====================== 示例 main(演示常用 API) ======================
输入:
n
x1 y1
...
xn yn
q
q 行点坐标,查询“点在多边形内”结果
输出:
- 多边形面积、周长、重心
- 凸包点数与直径长度
- 每个查询点的 IN/ON/OUT
提示:
- 根据题目需要可替换/扩展 main;上方库函数可直接复用
*/
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
Poly pg(n);
for (int i = 0; i < n; ++i) cin >> pg[i].x >> pg[i].y;
cout.setf(ios::fixed); cout << setprecision(10);
// 多边形基础量
cout << "area " << area(pg) << '\n';
cout << "perim " << perim(pg) << '\n';
P cen = centroid(pg);
cout << "centroid " << cen.x << ' ' << cen.y << '\n';
// 凸包与直径
Poly ch = hull(pg);
auto [i, j, d2] = dia_convex(ch);
cout << "hull_sz " << (int)ch.size() << '\n';
cout << "diameter " << sqrt(max(0.0, d2)) << '\n';
// 点内判定演示
int q; cin >> q;
while (q--) {
P p; cin >> p.x >> p.y;
int res = in_poly(pg, p);
cout << (res == 2 ? "IN" : res == 1 ? "ON" : "OUT") << '\n';
}
return 0;
}
/*
使用示例:
输入:
5
0 0
4 0
5 2
2 4
-1 2
3
2 2
5 5
0 0
输出(可能之一):
area 16.0000000000
perim 16.9442719100
centroid 2.0000000000 1.8000000000
hull_sz 5
diameter 5.0000000000
IN
OUT
ON
更多用法提示:
- 线段最短距离:dist_ss({A,B},{C,D})
- 点到线/线段距离:dist_pl(P,L)、dist_ps(P,Seg)
- 线/段交点:inter_ll、inter_ss_point
- 圆与线/段/圆交:inter_cl、inter_cs、inter_cc
- 点到圆切线:tan_pc;两圆公切线:tan_cc
- 圆交面积:inter_area_cc
- 半平面裁剪(单条直线):cut_poly
*/