板子合集(自用)
计算几何
线段平行
设线段端点分别为$P_1,P_2$和$P_3,P_4$
则$$\overrightarrow{P_1 P_2} \times \overrightarrow{P_3 P_4} = 0$$
线段共线
设线段端点分别为$P_1,P_2$和$P_3,P_4$
则$$\overrightarrow{P_1 P_2} \times \overrightarrow{P_2 P_3} = 0$$
$$\overrightarrow{P_1 P_2} \times \overrightarrow{P_2 P_4} = 0$$
判断一个点在线段的哪边
假设有向线段为$\overrightarrow{AB}$ ,点为$C$,首先计算外积$\overrightarrow{AC} × \overrightarrow{AB}=( \overrightarrow{AC}_{x} ⋅\overrightarrow{AB}_{y}-\overrightarrow{AC}_{y} ⋅\overrightarrow{AB}_{x})\overrightarrow{k}$
(因为有$\overrightarrow{AB}_{z}=0$,$\overrightarrow{AB}_{z}=0$
)。根据右手螺旋定则,如果$\overrightarrow{k}$的系数为正数,说明点$C$在线段$AB$的右侧;如果为负数,说明点$C$在线段$AB$的左侧;如果为0,说明点$C$在线段$AB$所在的直线上。
//*************************************************************************
// \brief: 计算两个向量的外积(叉乘)。可以根据结果的符号判断三个点的位置关系。
// \Param: Point A 两个向量的公共起点。
// \Param: Point B 第一个向量的终点。
// \Param: Point C 第二个向量的终点。
// \Returns: double 向量AC与向量AB的外积。
//如果结果为正数,表明点C在直线AB(直线方向为从A到B)的右侧;
//如果结果为负数,表明点C在直线AB(直线方向为从A到B)的左侧;
//如果结果为0,表明点C在直线AB上。
//*************************************************************************
double cross(Point A, Point B, Point C) {
double cross1 = (C.x - A.x) * (B.y - A.y);
double cross2 = (C.y - A.y) * (B.x - A.x);
return (cross1 - cross2);
}
线段相交
跨立实验
T1 = cross(A1, A2, B1);
T2 = cross(A1, A2, B2);
T3 = cross(B1, B2, A1);
T4 = cross(B1, B2, A2);
快速排斥实验
(1). 如果$(T1 * T2) > 0$ || $(T3 * T4) > 0$,说明一条线段的两个端点在另一条线段的同侧,这两条线段肯定不相交。
(2). 如果$T1 == 0$ && $T2 == 0$,说明两条线段共线,是否相交还需要进一步判断。这时可以通过判断两条线段张成的矩形是否相交来判断,而两个矩形是否相交可以通过快速排斥实验来判断。
//*************************************************************************
// \brief: 快速排斥实验,判断两个线段张成的矩形区域是否相交。
// \Param: Point S1 第一条线段的起点。
// \Param: Point E1 第一条线段的终点。
// \Param: Point S2 第二条线段的起点。
// \Param: Point E2 第二条线段的终点。
// \Returns: bool 两个线段张成的矩形区域是否相交。
//具有对称性,即交换两条线段(参数S1与S2交换、E1与E2交换),结果不变。
//*************************************************************************
bool rectsIntersect(Point S1, Point E1, Point S2, Point E2) {
if ( min(S1.y, E1.y) <= max(S2.y, E2.y) && max(S1.y, E1.y) >= min(S2.y, E2.y)
&& min(S1.x, E1.x) <= max(S2.x, E2.x) && max(S1.x, E1.x) >= min(S2.x, E2.x)) {
return true; } return false;
}
(3). 其他情况,两个线段一定相交。
代码实现:
bool segmentsIntersect(Point A1, Point A2, Point B1, Point B2) {
double T1 = cross(A1, A2, B1);
double T2 = cross(A1, A2, B2);
double T3 = cross(B1, B2, A1);
double T4 = cross(B1, B2, A2);
if (((T1 * T2) > 0) || ((T3 * T4) > 0)) {
// 一条线段的两个端点在另一条线段的同侧,不相交。
//(可能需要额外处理以防止乘法溢出,视具体情况而定。)
return false;
} else if(T1 == 0 && T2 == 0) {
// 两条线段共线,利用快速排斥实验进一步判断。此时必有 T3 == 0 && T4 == 0。
return rectsIntersect(A1, A2, B1, B2);
} else { // 其它情况,两条线段相交。
return true;
}
}
可以看到,这种方法不需要对线段的起终点重合(线段退化为一个点)做特殊判断,也不需要对线段平行(除了共线的情况)做特殊判断。纯几何方法,逻辑更简洁。
线段交点
参考文献:计算几何之两条线段的交点 - Huntto - 博客园 (cnblogs.com)
设线段端点分别为$P_1,P_2$和$P_3,P_4$,交点为$P_0$,$$P_0 = P_1+t * \overrightarrow{P_1 P_2} $$ $$P_0 = P_3+s * \overrightarrow{P_3 P_4} $$
将点坐标代入公式$$\begin{cases}x_1+(x_2-x_1)t=x_3+(x_4-x_3)s \\ \\ y_1+(y_2-y_1)t=y_3+(y_4-y_3)s \end{cases}$$
解方程得$$\begin{cases}t=\frac{(x_3-x_1)(y_4-y_3)-(y_3-y_1)(x_4-x_3)}{(x_2-x_1)(y_4-y_3)-(y_2-y_1)(x_4-x_3)} \\ \\ s=\frac{(x_3-x_1)(y_2-y_1)-(y_3-y_1)(x_2-x_1)}{(x_2-x_1)(y_4-y_3)-(y_2-y_1)(x_4-x_3)} \end{cases}$$
写作向量形式为
$$\begin{align*}t=\frac{\overrightarrow{P_3 P_1} \times \overrightarrow{P_4 P_3}}{\overrightarrow{P_2 P_1} \times \overrightarrow{P_4 P_3}} \\s=\frac{\overrightarrow{P_3 P_1} \times \overrightarrow{P_2 P_1}}{\overrightarrow{P_2 P_1} \times \overrightarrow{P_4 P_3}} \end{align*}$$
因此$P_0$的坐标为$$\begin{cases}x_0 = x_1+t * (x_2-x_1) \\y_0 = y_1+t * (y_2-y_1) \end{cases}$$
或$$\begin{cases}x_0 = x_3+s * (x_4-x_3) \\y_0 = y_3+s * (y_4-y_3) \end{cases}$$
向量旋转
设$\overrightarrow{a} = (x,y)$,逆时针旋转$\alpha$角得到
$$\overrightarrow{b} =(x\cos\alpha −y\sin\alpha ,y\cos\alpha+x\sin\alpha )$$
可以用复数的乘法证明。
三角剖分求面积
把相邻每两个顶点与原点构成的向量的叉积的数值的一半依次累加起来,就能得到多边形的面积。
$$ S_{ABCDEF} = \frac{\overrightarrow{OA} × \overrightarrow{OB} + \overrightarrow{OB} × \overrightarrow{OC} +⋯+ \overrightarrow{OF} × \overrightarrow{OA}}{2} $$
凸包
首先把所有点以横坐标为第一关键字,纵坐标为第二关键字排序。
显然排序后最小的元素和最大的元素一定在凸包上。而且因为是凸多边形,我们如果从一个点出发逆时针走,轨迹总是 “左拐” 的,一旦出现右拐,就说明这一段不在凸包上。因此我们可以用一个单调栈来维护上下凸壳。
因为从左向右看,上下凸壳所旋转的方向不同,为了让单调栈起作用,我们首先升序枚举求出下凸壳,然后降序求出上凸壳。
求凸壳时,一旦发现即将进栈的点($P$)和栈顶的两个点($S1,S2$,其中 $S1$ 为栈顶 )行进的方向向右旋转,即叉积小于 00:$\overrightarrow{S_{2}S_{1}}×\overrightarrow{S_{1}P}<0$,则弹出栈顶,回到上一步,继续检测,直到 $\overrightarrow{S_{2}S_{1}}×\overrightarrow{S_{1}P}\ge 0$或者栈内仅剩一个元素为止。
通常情况下不需要保留位于凸包边上的点,因此上面一段中 $\overrightarrow{S_{2}S_{1}}×\overrightarrow{S_{1}P}<0$ 这个条件中的 “$<$” 可以视情况改为 $\le$,同时后面一个条件应改为 $>$。
最后不要忘了把最小的元素与栈顶进行比较,以保证最后一段也是凸壳。
时间复杂度 $O(n\log_{}{n} )$。
sort(p+1,p+1+n);
stk[++tp]=1;
for(int i=2;i<=n;++i) {
while(tp>1&&(p[stk[tp]]-p[stk[tp-1]])*(p[i]-p[stk[tp]])<=0)
[stk[tp--]]=0;
used[i]=1;
stk[++tp]=i;
}
int qaq=tp;
for(int i=n-1;i>0;--i)
if(!used[i]) {
while(tp>qaq&&(p[stk[tp]]-p[stk[tp-1]])*(p[i]-p[stk[tp]])<=0)
used[stk[tp--]]=0;
used[i]=1;
stk[++tp]=i;
}
for(int i=1;i<=tp;++i)
h[i]=p[stk[i]];
eps误差处理
$$a==0 : fabs(a)<eps$$ $$a<0 : a<−eps$$ $$a>0 : a>eps$$ $$a<=0 : a<eps$$ $$a>=0 : a>-eps$$
在一些卡精度的题(如UVA10173),允许的情况下建议使用long double
注意long double
对应 %Lf
旋转卡壳
旋转卡壳是用来求凸包直径的算法。
对踵点
如果过凸多边形上两点作一对平行线,使得整个多边形都在这两条线之间,那么这两个点被称为一对对踵点。
凸多边形的直径
即凸多边形上任意两个点之间距离的最大值。直径一定会在对踵点中产生。
算法思路
我们以凸包上的每条边为对象考虑,求出每一条边的对踵点。
点到边的距离可以用三角形面积*2除以底边长度来得到,而三角形面积可以通过叉积求解,又注意到对踵点在逆时针枚举边的过程中也逆时针移动,所以对于所有边的对踵点,我们使用双指针法求解。
难点在于编码细节极其多。
struct vec {
ll x, y;
friend vec operator -(const vec &a, const vec &b) {
return vec{a.x - b.x, a.y - b.y};
}
friend ll operator *(const vec &a, const vec &b) {
return (a.x * b.y - a.y * b.x);
}
ll dis_() {
return x * x + y * y;
}
friend ll dis(const vec &a, const vec &b, const vec &x) {
return abs((x - a) * (x - b));
}
friend bool operator <(const vec &a, const vec &b) {
return (a.x < b.x) || (a.x == b.x && a.y < b.y);
}
} p[100005];
int n, st[100005], top = 0;
char used[100005];
inline void convexhull() { // 求凸包
sort(p + 1, p + 1 + n);
for (int i = 1; i <= n; i++) {
while (top > 1 && (p[st[top]] - p[st[top - 1]]) *
(p[i] - p[st[top]]) <= 0)
used[st[top--]] = 0;
st[++top] = i;
used[i] = 1;
}
int qaq = top;
for (int i = n - 1; i > 1; i--) {
if (!used[i]) {
while (top > qaq && (p[st[top]] - p[st[top - 1]]) *
(p[i] - p[st[top]]) <= 0)
used[st[top--]] = 0;
st[++top] = i;
used[i] = 1;
}
}
while (top > qaq && (p[st[top]] - p[st[top - 1]]) *
(p[1] - p[st[top]]) <= 0)
used[st[top--]] = 0;
st[++top] = 1;
}
inline ll rotating() {
if (top < 4) // 注意点1会在开头和结尾各自入栈一次
return (p[st[1]] - p[st[2]]).dis_(); // 如果凸包只有两个点直接输出两点距离
int t = 3;
ll mx = 0;
for (int i = 1; i < top; i++) {
while (dis(p[st[i]], p[st[i + 1]], p[st[t]]) <=
dis(p[st[i]], p[st[i + 1]], p[st[t % top + 1]]))
t = t % top + 1;
mx = max(mx, max((p[st[t]] - p[st[i]]).dis_(),
(p[st[t]] - p[st[i + 1]]).dis_()));
}
return mx;
}
例题:Smallest Bounding Rectangle - UVA 10173
给出平面上的一堆点,找出一个能够覆盖所有点的面积最小的矩形,输出面积及四个顶点的坐标。
首先有结论:这样的矩形一定有一条边与凸包重合。
那么我们对于每一条边,只需要求出对踵点,最左点和最右点。
最左点和最右点的求法和对踵点类似,我们只需要改用点积来判断即可。
求出这三点后,我们先求出底边的垂线,就可以结合左右两点坐标求出水平长度,再结合边到对踵点的高度求出面积。
#define double long double
typedef long long ll;
const int MAXN = 100001;
int n;
const double eps = 1e-8;
struct vec {
double x, y;
friend vec operator +(vec x, vec y) {
return vec{x.x + y.x, x.y + y.y};
}
friend vec operator -(vec x, vec y) {
return vec{x.x - y.x, x.y - y.y};
}
friend double operator *(vec x, vec y) {
return (x.x * y.y - x.y * y.x);
}
friend double operator ^(vec x, vec y) {
return (x.x * y.x + x.y * y.y);
}
friend bool operator <(vec x, vec y) {
return (x.x - y.x < eps) ||
(fabs(x.x - y.x) < eps && x.y - y.y < eps);
}
double dis() {
return sqrt(x * x + y * y);
}
} p[MAXN], h[MAXN];
int st[MAXN], top;
char used[MAXN];
inline void convexhull() {
top = 0;
memset(used, 0, n + 1);
sort(p + 1, p + 1 + n);
st[++top] = 1;
for (int i = 2; i <= n; i++) {
while (top > 1 && (p[st[top]] - p[st[top - 1]]) *
(p[i] - p[st[top]]) < eps)
used[st[top--]] = 0;
st[++top] = i;
used[i] = 1;
}
for (int i = n - 1; i >= 1; i--) {
if (!used[i]) {
while (top > 1 && (p[st[top]] - p[st[top - 1]]) *
(p[i] - p[st[top]]) < eps)
used[st[top--]] = 0;
st[++top] = i;
used[i] = 1;
}
}
for (int i = 1; i <= top; i++)
h[i] = p[st[i]];
}
inline double sqr(vec a, vec b, vec x) {
return (a - x) * (b - x);
}
inline double solve() {
if (top < 4) return 0;
int t = 3;
while (sqr(h[1], h[2], h[t]) -
sqr(h[1], h[2], h[t % top + 1]) < eps)
t = t % top + 1;
int t2 = 1, t3 = t;
double ans = 1e18;
for (int i = 1; i < top; i++) {
while (sqr(h[i], h[i + 1], h[t]) -
sqr(h[i], h[i + 1], h[t % top + 1]) < eps)
t = t % top + 1;
while (((h[i + 1] - h[i]) ^ (h[t2 % top + 1] - h[t2])) > -eps)
t2 = t2 % top + 1; // 记得都要%top再+1
while (((h[i + 1] - h[i]) ^ (h[t3 % top + 1] - h[t3])) < eps)
t3 = t3 % top + 1;
vec vert = {-(h[i + 1].y - h[i].y), h[i + 1].x - h[i].x};
double ar = (sqr(h[i], h[i + 1], h[t]) /
(h[i] - h[i + 1]).dis()) *
(sqr(h[t2], h[t2] + vert, h[t3]) /
(vert).dis());
ans = min(ans, ar);
}
return ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
while (cin >> n) {
if (!n) return 0;
for (int i = 1; i <= n; i++)
cin >> p[i].x >> p[i].y;
convexhull();
printf("%.4Lf\n", solve());
}
return 0;
}
自适应辛普森法
Simpson公式:用抛物线来拟合原函数。$$\int\limits_{a}^{b}f(x)dx=\frac{1}{6}(b-a)[f(a)+f(b)+4f(\frac{a+b}{2})]$$
通过不断将区间二分直到满足需要的精度。
例题:BZOJ-2178 圆的面积并
用自适应辛普森法求解,$f(x_{0})$即直线$x=x_{0}$与所有圆的相交的$y$坐标区间的长度。
trick:注意这道题看似对精度要求不高,实则测试点13会卡精度,所以为了避免被特意构造的数据卡掉,我们将坐标系旋转一个随机角度。这是一个常用技巧,在旋转卡壳中也可以使用。
#define double long double
int n;
double x[1005],y[1005],r[1005];
typedef pair<double,double> P;
P q[1005];
inline double f(double v){
int tot=0;
for(int i=1;i<=n;i++){
double dx=x[i]-v;
if(r[i]*r[i]-dx*dx>1e-8){
double dy=sqrt(r[i]*r[i]-dx*dx);
q[++tot]=P(y[i]-dy,y[i]+dy);
}
}
if(!tot) return 0;
sort(q+1,q+1+tot);
//以下将x=v穿过的所有y区间进行合并
double ans=0,st=q[1].first,ed=q[1].second;
for(int i=2;i<=tot;i++){
if(q[i].first<=ed)
ed=max(ed,q[i].second);
else{
ans+=ed-st;
st=q[i].first;
ed=q[i].second;
}
}
return ans+ed-st;
}
inline double simpson(double l,double r){
return (f(l)+f(r)+4*f((l+r)/2))*(r-l)/6;
}
inline double asr(double l,double r,double eps,double ans){
double mid=(l+r)/2;
double ansl=simpson(l,mid),ansr=simpson(mid,r);
if(fabs(ansl+ansr-ans)<15*eps)
return ansl+ansr+(ansl+ansr-ans)/15;
return asr(l,mid,eps/2,ansl)+
asr(mid,r,eps/2,ansr);
}
void _rotate(double &x, double &y,double angle)
{
double x_=x;
double y_=y;
x=x_ * cos(angle) + y_ * sin(angle);
y=-x_ * sin(angle) + y_ * cos(angle);
}
int main(){
ios::sync_with_stdio(0);cin.tie(0);
cin>>n>>x[1]>>y[1]>>r[1];
int rot=1; //坐标系旋转一个随机角度
_rotate(x[1],y[1],rot);
double lf=x[1]-r[1],rf=x[1]+r[1];
for(int i=2;i<=n;i++){
cin>>x[i]>>y[i]>>r[i];
_rotate(x[i],y[i],rot);
lf=min(x[i]-r[i],lf);
rf=max(x[i]+r[i],rf);
}
printf("%.3Lf",asr(lf-10,rf+10,1e-5,
simpson(lf-10,rf+10)));
return 0;
}
图论
最大流
struct MF {
struct edge {
int v, nxt, cap, flow;
} e[N];
int fir[N], cnt = 0;
int n, S, T;
ll maxflow = 0;
int dep[N], cur[N];
void init() {
memset(fir, -1, sizeof fir);
cnt = 0;
}
void addedge(int u, int v, int w) {
e[cnt] = {v, fir[u], w, 0};
fir[u] = cnt++;
e[cnt] = {u, fir[v], 0, 0};
fir[v] = cnt++;
}
bool bfs() {
queue<int> q;
memset(dep, 0, sizeof(int) * (n + 1));
dep[S] = 1;
q.push(S);
while (q.size()) {
int u = q.front();
q.pop();
for (int i = fir[u]; ~i; i = e[i].nxt) {
int v = e[i].v;
if ((!dep[v]) &&
(e[i].cap > e[i].flow)) {
dep[v] = dep[u] + 1;
q.push(v);
}
}
}
return dep[T];
}
int dfs(int u, int flow) {
if ((u == T) || (!flow)) return flow;
int ret = 0;
for (int& i = cur[u]; ~i; i = e[i].nxt) {
int v = e[i].v, d;
if ((dep[v] == dep[u] + 1) &&
(d = dfs(v, min(flow - ret,
e[i].cap - e[i].flow)))) {
ret += d;
e[i].flow += d;
e[i ^ 1].flow -= d;
if (ret == flow) return ret;
}
}
return ret;
}
void dinic() {
while (bfs()) {
memcpy(cur, fir, sizeof(int) *
(n + 1));
maxflow += dfs(S, INF);
}
}
} mf;
费用流
constexpr int INF = 0x3f3f3f3f;
using namespace std;
struct edge {
int v, f, c, next;
} e[100005];
struct node {
int v, e;
} p[10005];
struct mypair {
int dis, id;
bool operator<(const mypair& a)
const { return dis > a.dis; }
mypair(int d, int x) { dis = d, id = x; }
};
int head[5005], dis[5005], vis[5005], h[5005];
int n, m, s, t, cnt = 1, maxf, minc;
void addedge(int u, int v, int f, int c) {
e[++cnt].v = v;
e[cnt].f = f;
e[cnt].c = c;
e[cnt].next = head[u];
head[u] = cnt;
}
bool dijkstra() {
priority_queue<mypair> q;
for (int i = 1; i <= n; i++) dis[i] = INF;
memset(vis, 0, sizeof(vis));
dis[s] = 0;
q.push(mypair(0, s));
while (!q.empty()) {
int u = q.top().id;
q.pop();
if (vis[u]) continue;
vis[u] = 1;
for (int i = head[u]; i; i = e[i].next) {
int v = e[i].v,
nc = e[i].c + h[u] - h[v];
if (e[i].f && dis[v] > dis[u] + nc) {
dis[v] = dis[u] + nc;
p[v].v = u;
p[v].e = i;
if (!vis[v])
q.push(mypair(dis[v], v));
}
}
}
return dis[t] != INF;
}
void spfa() {
queue<int> q;
memset(h, 63, sizeof(h));
h[s] = 0, vis[s] = 1;
q.push(s);
while (!q.empty()) {
int u = q.front();
q.pop();
vis[u] = 0;
for (int i = head[u]; i; i = e[i].next) {
int v = e[i].v;
if (e[i].f && h[v] > h[u] + e[i].c) {
h[v] = h[u] + e[i].c;
if (!vis[v]) {
vis[v] = 1;
q.push(v);
}
}
}
}
}
int main() {
scanf("%d%d%d%d", &n, &m, &s, &t);
for (int i = 1; i <= m; i++) {
int u, v, f, c;
scanf("%d%d%d%d", &u, &v, &f, &c);
addedge(u, v, f, c);
addedge(v, u, 0, -c);
}
spfa(); // 先求出初始势能
while (dijkstra()) {
int minf = INF;
for (int i = 1; i <= n; i++)
h[i] += dis[i];
for (int i = t; i != s; i = p[i].v)
minf = min(minf, e[p[i].e].f);
for (int i = t; i != s; i = p[i].v) {
e[p[i].e].f -= minf;
e[p[i].e ^ 1].f += minf;
}
maxf += minf;
minc += minf * h[t];
}
printf("%d %d\n", maxf, minc);
return 0;
}
二分图匹配
最小点覆盖:选最少的点,满足每条边至少有一个端点被选。
最大独立集:选最多的点,满足两两之间没有边相连。
二分图最小点覆盖 = 最大匹配,最大独立集 = n - 最小点覆盖。
struct augment_path {
vector<vector<int>> g;
vector<int> pa; // 匹配
vector<int> pb;
vector<int> vis; // 访问
int n, m; // 两个点集中的顶点数量
int dfn; // 时间戳记
int res; // 匹配数
augment_path(int _n, int _m) : n(_n), m(_m){
assert(0 <= n && 0 <= m);
pa = vector<int>(n, -1);
pb = vector<int>(m, -1);
vis = vector<int>(n);
g.resize(n);
res = 0;
dfn = 0;
}
void add(int from, int to) {
assert(0 <= from && from < n &&
0 <= to && to < m);
g[from].push_back(to);
}
bool dfs(int v) {
vis[v] = dfn;
for (int u : g[v]) {
if (pb[u] == -1) {
pb[u] = v;
pa[v] = u;
return true;
}
}
for (int u : g[v]) {
if (vis[pb[u]] != dfn
&& dfs(pb[u])) {
pa[v] = u;
pb[u] = v;
return true;
}
}
return false;
}
int solve() {
while (true) {
dfn++;
int cnt = 0;
for (int i = 0; i < n; i++) {
if (pa[i] == -1 && dfs(i)) {
cnt++;
}
}
if (cnt == 0) {
break;
}
res += cnt;
}
return res;
}
};
2-SAT
假设有 a1,a2 和 b1,b2 两对,已知 a1和 b2 间有矛盾,于是为了方案自治,由于两者中必须选个,所以我们就要拉两条有向边(a1,61)和(b2,a2)表示选了 a1则必须选 b1,选了 b2 则必须选a2才能够自治。 然后通过这样子建边我们跑一遍 Tarian Scc判断是否有一个集合中的两个元素在同一个 SCC中若有则输出不可能,否则输出方案。构造方案只需要把几个不矛盾的 SCC拼起来就好了。 输出方案时可以通过变量在图中的拓扑序确定该变量的取值。如果变量x的拓扑序在 -x 之后,那么取x值为真。应用到 Tarian 算法的缩点,即:所在 SCC编号在 -x 之前时,取x为真。因为Tarjan 算法求强连通分量时使用了栈,所以 Tarian 求得的 SCC编号相当于反拓扑序。 显然地,时间复杂度为 O(n + m)。
缩点
int dfn[N], low[N], dfncnt,
s[N], in_stack[N], tp;
int scc[N], sc; // 结点 i 所在 SCC 的编号
int sz[N]; // 强连通 i 的大小
void tarjan(int u) {
low[u] = dfn[u] = ++dfncnt,
s[++tp] = u, in_stack[u] = 1;
for (int i = h[u]; i; i = e[i].nex) {
const int &v = e[i].t;
if (!dfn[v]) {
tarjan(v);
low[u] = min(low[u], low[v]);
} else if (in_stack[v]) {
low[u] = min(low[u], dfn[v]);
}
}
if (dfn[u] == low[u]) {
++sc;
while (s[tp] != u) {
scc[s[tp]] = sc;
sz[sc]++;
in_stack[s[tp]] = 0;
--tp;
}
scc[s[tp]] = sc;
sz[sc]++;
in_stack[s[tp]] = 0;
--tp;
}
}
#include <algorithm>
#include <cstdio>
#include <vector>
constexpr int MN = 100005;
int N, M, cnt;
std::vector<int> G[MN], T[MN * 2];
int dfn[MN], low[MN], dfc;
int stk[MN], tp;
void Tarjan(int u) {
printf(" Enter : #%d\n", u);
low[u] = dfn[u] = ++dfc; // low 初始化为当前节点 dfn
stk[++tp] = u; // 加入栈中
for (int v : G[u]) { // 遍历 u 的相邻节点
if (!dfn[v]) { // 如果未访问过
Tarjan(v); // 递归
low[u] = std::min(low[u], low[v]); // 未访问的和 low 取 min
if (low[v] == dfn[u]) { // 找到一个以 u 为根的 BCC
++cnt; // 增加方点个数
printf(" Found a New BCC #%d.\n", cnt - N);
// 将点双中除了 u 的点退栈,并在圆方树中连边
for (int x = 0; x != v; --tp) {
x = stk[tp];
T[cnt].push_back(x);
T[x].push_back(cnt);
printf(" BCC #%d has vertex #%d\n", cnt - N, x);
}
// 注意 u 自身也要连边(但不退栈)
T[cnt].push_back(u);
T[u].push_back(cnt);
printf(" BCC #%d has vertex #%d\n", cnt - N, u);
}
} else
low[u] = std::min(low[u], dfn[v]); // 已访问的和 dfn 取 min
}
printf(" Exit : #%d : low = %d\n", u, low[u]);
printf(" Stack:\n ");
for (int i = 1; i <= tp; ++i)
printf("%d, ", stk[i]);
puts("");
}
int main() {
scanf("%d%d", &N, &M);
cnt = N; // 点双 / 方点标号从 N 开始
for (int i = 1; i <= M; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G[u].push_back(v); // 加双向边
G[v].push_back(u);
}
// 处理非连通图
for (int u = 1; u <= N; ++u)
if (!dfn[u]) Tarjan(u), --tp;
// 注意到退出 Tarjan 时栈中还有一个元素即根,将其退栈
return 0;
}
普通环计数
状压
#include <iostream>
using namespace std;
int n, m;
struct Edge {
int to, nxt;
} edge[500];
int cntEdge, head[20];
void addEdge(int u, int v) {
edge[++cntEdge] = {v, head[u]}, head[u] = cntEdge;
}
long long answer, dp[1 << 19][20];
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
addEdge(u, v);
addEdge(v, u);
}
for (int i = 1; i <= n; i++)
dp[1 << (i - 1)][i] = 1;
for (int s = 1; s < (1 << n); s++)
for (int i = 1; i <= n; i++) {
if (!dp[s][i]) continue;
for (int j = head[i]; j; j = edge[j].nxt) {
int u = i, v = edge[j].to;
if ((s & -s) > (1 << (v - 1))) continue;
if (s & (1 << (v - 1))) {
if ((s & -s) == (1 << (v - 1)))
answer += dp[s][u];
} else
dp[s | (1 << (v - 1))][v] += dp[s][u];
}
}
cout << (answer - m) / 2 << '\n';
return 0;
}
三元环计数
#include <iostream>
using namespace std;
int n, m, total;
int deg[200001], u[200001], v[200001];
bool vis[200001];
struct Edge {
int to, nxt;
} edge[200001];
int cntEdge, head[100001];
void addEdge(int u, int v) {
edge[++cntEdge] = {v, head[u]}, head[u] = cntEdge;
}
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
cin >> n >> m;
for (int i = 1; i <= m; i++)
cin >> u[i] >> v[i], deg[u[i]]++, deg[v[i]]++;
for (int i = 1; i <= m; i++) {
if ((deg[u[i]] == deg[v[i]] && u[i] > v[i]) ||
deg[u[i]] < deg[v[i]])
swap(u[i], v[i]);
addEdge(u[i], v[i]);
}
for (int u = 1; u <= n; u++) {
for (int i = head[u]; i; i = edge[i].nxt)
vis[edge[i].to] = true;
for (int i = head[u]; i; i = edge[i].nxt) {
int v = edge[i].to;
for (int j = head[v]; j; j = edge[j].nxt) {
int w = edge[j].to;
if (vis[w]) total++;
}
}
for (int i = head[u]; i; i = edge[i].nxt)
vis[edge[i].to] = false;
}
cout << total << '\n';
return 0;
}
四元环计数
#include <iostream>
#include <vector>
using namespace std;
int n, m, deg[100001], cnt[100001];
vector<int> E[100001], E1[100001];
long long total;
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
E[u].push_back(v);
E[v].push_back(u);
deg[u]++, deg[v]++;
}
for (int u = 1; u <= n; u++)
for (int v : E[u])
if (deg[u] > deg[v] || (deg[u] == deg[v] && u > v))
E1[u].push_back(v);
for (int a = 1; a <= n; a++) {
for (int b : E1[a])
for (int c : E[b]) {
if (deg[a] < deg[c] || (deg[a] == deg[c] && a <= c))
continue;
total += cnt[c]++;
}
for (int b : E1[a])
for (int c : E[b])
cnt[c] = 0;
}
cout << total << '\n';
return 0;
}
LCA
constexpr int MXN = 40005;
using namespace std;
vector<int> v[MXN];
vector<int> w[MXN];
int fa[MXN][31], cost[MXN][31], dep[MXN];
int n, m;
int a, b, c;
void dfs(int root, int fno) {
fa[root][0] = fno;
dep[root] = dep[fa[root][0]] + 1;
for (int i = 1; i < 31; ++i) {
fa[root][i] = fa[fa[root][i - 1]][i - 1];
cost[root][i] = cost[fa[root][i - 1]][i - 1] + cost[root][i - 1];
}
int sz = v[root].size();
for (int i = 0; i < sz; ++i) {
if (v[root][i] == fno) continue;
cost[v[root][i]][0] = w[root][i];
dfs(v[root][i], root);
}
}
int lca(int x, int y) {
if (dep[x] > dep[y]) swap(x, y);
int tmp = dep[y] - dep[x];
for (int j = 0; tmp; ++j, tmp >>= 1)
if (tmp & 1) {
y = fa[y][j];
}
if (y == x) return 0;
for (int j = 30; j >= 0 && y != x; --j) {
if (fa[x][j] != fa[y][j]) {
x = fa[x][j];
y = fa[y][j];
}
}
return cost[x][0] + cost[y][0];
}
树的直径
constexpr int N = 10000 + 10;
int n, c, d[N];
vector<int> E[N];
void dfs(int u, int fa) {
for (int v : E[u]) {
if (v == fa) continue;
d[v] = d[u] + 1;
if (d[v] > d[c]) c = v;
dfs(v, u);
}
}
int main() {
scanf("%d", &n);
for (int i = 1; i < n; i++) {
int u, v;
scanf("%d %d", &u, &v);
E[u].push_back(v), E[v].push_back(u);
}
dfs(1, 0);
d[c] = 0, dfs(c, 0);
printf("%d\n", d[c]);
return 0;
}
树的中心
在树中,如果节点 x作为根节点时,从 x 出发的最长链最短,那么称 x 为这棵树的中心。
- 维护
len1x
,表示节点x子树内的最长链。 - 维护
len2x
,表示不与len1x
重叠的最长链。 - 维护
upx
,表示节点x子树外的最长链,该链必定经过x的父节点。 - 找到点x使得
max(len1x, upx)
最小,那么x即为树的中心。
// 这份代码默认节点编号从 1 开始,即 i ∈ [1,n],使用vector存图
int d1[N], d2[N], up[N], x, y, mini = 1e9; // d1,d2对应上文中的len1,len2
struct node {
int to, val; // to为边指向的节点,val为边权
};
vector<node> nbr[N];
void dfsd(int cur, int fa) { // 求取len1和len2
for (node nxtn : nbr[cur]) {
int nxt = nxtn.to, w = nxtn.val; // nxt为这条边通向的节点,val为边权
if (nxt == fa) {
continue;
}
dfsd(nxt, cur);
if (d1[nxt] + w > d1[cur]) { // 可以更新最长链
d2[cur] = d1[cur];
d1[cur] = d1[nxt] + w;
} else if (d1[nxt] + w > d2[cur]) { // 不能更新最长链,但可更新次长链
d2[cur] = d1[nxt] + w;
}
}
}
void dfsu(int cur, int fa) {
for (node nxtn : nbr[cur]) {
int nxt = nxtn.to, w = nxtn.val;
if (nxt == fa) {
continue;
}
up[nxt] = up[cur] + w;
if (d1[nxt] + w != d1[cur]) { // 如果自己子树里的最长链不在nxt子树里
up[nxt] = max(up[nxt], d1[cur] + w);
} else { // 自己子树里的最长链在nxt子树里,只能使用次长链
up[nxt] = max(up[nxt], d2[cur] + w);
}
dfsu(nxt, cur);
}
}
void GetTreeCenter() { // 统计树的中心,记为x和y(若存在)
dfsd(1, 0);
dfsu(1, 0);
for (int i = 1; i <= n; i++) {
if (max(d1[i], up[i]) < mini) { // 找到了当前max(len1[x],up[x])最小点
mini = max(d1[i], up[i]);
x = i;
y = 0;
} else if (max(d1[i], up[i]) == mini) { // 另一个中心
y = i;
}
}
}
树的重心
如果在树中选择某个节点并删除,这棵树将分为若干棵子树,统计子树节点数并记录最大值。取遍树上所有节点,使此最大值取到最小的节点被称为整个树的重心。
// 这份代码默认节点编号从 1 开始,即 i ∈ [1,n]
int size[MAXN], // 这个节点的「大小」(所有子树上节点数 + 该节点)
weight[MAXN], // 这个节点的「重量」,即所有子树「大小」的最大值
centroid[2]; // 用于记录树的重心(存的是节点编号)
void GetCentroid(int cur, int fa) {
size[cur] = 1;
weight[cur] = 0;
for (int i = head[cur]; i != -1; i = e[i].nxt) {
if (e[i].to != fa) {
GetCentroid(e[i].to, cur);
size[cur] += size[e[i].to];
weight[cur] = max(weight[cur], size[e[i].to]);
}
}
weight[cur] = max(weight[cur], n - size[cur]);
if (weight[cur] <= n / 2) {
centroid[centroid[0] != 0] = cur;
}
}
树链剖分
定义 重子节点 表示其子节点中子树最大的子结点。如果有多个子树最大的子结点,取其一。如果没有子节点,就无重子节点。
定义 轻子节点 表示剩余的所有子结点。
void dfs1(int o) {
son[o] = -1;
siz[o] = 1;
for (int j = h[o]; j; j = nxt[j])
if (!dep[p[j]]) {
dep[p[j]] = dep[o] + 1;
fa[p[j]] = o;
dfs1(p[j]);
siz[o] += siz[p[j]];
if (son[o] == -1 || siz[p[j]] > siz[son[o]]) son[o] = p[j];
}
}
void dfs2(int o, int t) {
top[o] = t;
cnt++;
dfn[o] = cnt;
rnk[cnt] = o;
if (son[o] == -1) return;
dfs2(son[o], t); // 优先对重儿子进行 DFS,可以保证同一条重链上的点 DFS 序连续
for (int j = h[o]; j; j = nxt[j])
if (p[j] != son[o] && p[j] != fa[o]) dfs2(p[j], p[j]);
}
路径查询
用树链剖分求树上两点路径权值和,伪代码如下:
TREE-PATH-SUM(u,v)
- tot ← 0
- while u.top is not v.top
- if u.top.deep < v.top.deep
- SWAP(u,v)
- tot ← tot + sum of values between u and u.top
- u ← u.top father
- tot ← tot + sum of values between u and v
- return tot
链上的DFS序是连续的,可以使用线段树、树状数组维护。
每次选择深度较大的链往上跳,直到两点在同一条链上。
同样的跳链结构适用于维护、统计路径上的其他信息。
子树维护
有时会要求,维护子树上的信息,譬如将以x为根的子树的所有结点的权值增加v。
在DFS搜索的时候,子树中的结点的DFS序是连续的。
每一个结点记录bottom表示所在子树连续区间末端的结点。
这样就把子树信息转化为连续的一段区间信息。
// st 是线段树结构体
int querymax(int x, int y) {
int ret = -inf, fx = top[x], fy = top[y];
while (fx != fy) {
if (dep[fx] >= dep[fy])
ret = max(ret, st.query1(1, 1, n, dfn[fx], dfn[x])), x = fa[fx];
else
ret = max(ret, st.query1(1, 1, n, dfn[fy], dfn[y])), y = fa[fy];
fx = top[x];
fy = top[y];
}
if (dfn[x] < dfn[y])
ret = max(ret, st.query1(1, 1, n, dfn[x], dfn[y]));
else
ret = max(ret, st.query1(1, 1, n, dfn[y], dfn[x]));
return ret;
}
dsu on tree
例:给出一棵 n 个节点以 1 为根的树,节点 u 的颜色为 $c_u$,现在对于每个结点 u 询问以 u 为根的子树里一共出现了多少种不同的颜色。
既然支持离线,考虑预处理后 $O(1)$ 输出答案。
我们用 $cnt_i$ 表示颜色 i 的出现次数,$ans_u$ 表示结点 u 的答案。
遍历一个节点 u,我们按以下的步骤进行遍历:
- 先遍历 u 的轻(非重)儿子,并计算答案,但不保留遍历后它对 cnt 数组的影响;
- 遍历它的重儿子,保留它对 cnt 数组的影响;
- 再次遍历 u 的轻儿子的子树结点,加入这些结点的贡献,以得到 u 的答案。
数论
线性筛
vector<int> pri;
bool not_prime[N];
// int phi[N]; 欧拉函数
// int mu[N]; 莫比乌斯函数
// int d[N]; i的约数个数
// int num[N]; i的最小质因子出现次数
// int g[N], f[N]; i的约束和,i的最小质因子0~k幂次和
void pre(int n) {
// phi[1] = 1;
// mu[1] = 1;
// d[1] = 1;
// g[1] = f[1] = 1;
for (int i = 2; i <= n; ++i) {
if (!not_prime[i]) {
pri.push_back(i);
// phi[i] = i - 1;
// mu[i] = -1;
// d[i] = 2;
// num[i] = 1;
// g[i] = i + 1;
// f[i] = i + 1;
}
for (int pri_j : pri) {
if (i * pri_j > n) break;
not_prime[i * pri_j] = true;
if (i % pri_j == 0) {
// phi[i * pri_j] = phi[i] * pri_j;
// mu[i * pri_j] = 0;
// num[i * pri_j] = num[i] + 1;
// d[i * pri_j] = d[i] / num[i * pri_j]
// * (num[i * pri_j] + 1);
// g[i * pri_j] = g[i] * pri_j + 1;
// f[i * pri_j] = f[i] / g[i]
// * g[i * pri_j];
break;
}
// phi[i * pri_j] = phi[i] * phi[pri_j];'
// mu[i * pri_j] = -mu[i];
// num[i * pri_j] = 1;
// d[i * pri_j] = d[i] * 2;
// f[i * pri_j] = f[i] * f[pri_j];
// g[i * pri_j] = 1 + pri_j;
}
}
}
莫比乌斯函数 $μ(n)$的定义如下:
- 如果 n=1,则 $μ(1)=1$ 。
- 如果 n 是一个正整数,且其质因数分解中没有重复的质因数(即 nn 是一个平方自由数),则 $μ(n)$ 的值为:
- $μ(n)=(−1)^k$,其中 k 是 n 的不同质因数的个数。
- 如果 n 不是平方自由数(即 n 的质因数分解中有某个质因数的指数大于1),则 $μ(n)=0$。
数论分块
数论分块可以快速计算一些含有除法向下取整的和式(即形如 $\sum_{i=1}^{n} f(i) g(\left\lfloor \frac{n}{i} \right\rfloor)$ 的和式)。当可以在 O(1) 内计算 $f(r) - f(l)$ 或已经预处理出 f 的前缀和时,数论分块就可以在 $O(\sqrt{n})$ 的时间内计算上述和式的值。
它主要利用了富比尼定理(Fubini’s theorem),将 n 相同的数打包同时计算。
数论分块结论
对于常数 n,使得式子$\lfloor \frac{n}{i} \rfloor = \lfloor \frac{n}{j} \rfloor$成立且满足 $i \leq j \leq n$ 的 j 值最大为:$\left\lfloor \frac{n}{\left\lfloor \frac{n}{i} \right\rfloor} \right\rfloor$
即值$\lfloor \frac{n}{i} \rfloor$所在块的右端点为 $\left\lfloor \frac{n}{\left\lfloor \frac{n}{i} \right\rfloor} \right\rfloor$。
数论分块的过程
大概如下:考虑和式
$$ \sum_{i=1}^n f(i) \left\lfloor \frac{n}{i} \right\rfloor $$
那么由于我们可以知道 $\left\lfloor \frac{n}{i} \right\rfloor$ 的值成一个块状分布(就是同样的值都聚集在连续的块中),那么就可以用数论分块加速计算,降低时间复杂度。
利用上述结论,我们先求出 f(i) 的前缀和(记作 $s(i) = \sum_{j=1}^i f(j)$),然后每次以 $[l, r] = \left[l, \left\lfloor \frac{n}{\left\lfloor \frac{n}{i} \right\rfloor} \right\rfloor\right]$ 为一块,分块求出贡献累加到结果中即可。
伪代码如下:
- Calculate s(i), the prefix sum of f(i).
- $l \leftarrow 1$
- $r \leftarrow 0$
- result $\leftarrow 0$
- while $l \leq n$ do:
- $r \leftarrow \left\lfloor \frac{n}{l} \right\rfloor$
- result $\leftarrow$ result $+ [s(r) - s(l - 1)] \times \left\lfloor \frac{n}{l} \right\rfloor$
- $l \leftarrow r + 1$
- end while
最终得到的 result 即为所求的和式。
例:$\sum_{i=1}^n \left\lfloor \frac{n}{i} \right\rfloor$
long long H(int n) {
long long res = 0;
int l = 1, r;
while (l <= n) {
r = n / (n / l);
res += 1LL * (r - l + 1) * (n / l);
l = r + 1;
}
return res;
}
向上取整数论分块
对于常数 n,使得式子$\lceil \frac{n}{i} \rceil = \lceil \frac{n}{j} \rceil$成立且满足 $i \leq j \leq n$ 的 j 值最大为:$\left\lfloor \frac{n-1}{\left\lfloor \frac{n-1}{i} \right\rfloor} \right\rfloor$
即值$\lceil \frac{n}{i} \rceil = \lceil \frac{n}{j} \rceil$所在块的右端点为 $\left\lfloor \frac{n-1}{\left\lfloor \frac{n-1}{i} \right\rfloor} \right\rfloor$。
N 维数论分块
一般我们用的较多的是二维形式,式子中含有$\lfloor \frac{n}{i} \rfloor{和} \lfloor \frac{m}{i} \rfloor$,此时可将代码中 r = n / (n / i)
替换成 r = min(n / (n / i), m / (m / i))
。
extgcd
void exgcd(int a, int b, int& x, int& y) {
if (b == 0) {
x = 1, y = 0;
return;
}
exgcd(b, a % b, y, x);
y -= a / b * x;
}
CRT
LL CRT(int k, LL* a, LL* r) {
LL n = 1, ans = 0;
for (int i = 1; i <= k; i++)
n = n * r[i];
for (int i = 1; i <= k; i++) {
LL m = n / r[i], b, y;
exgcd(m, r[i], b, y); // b * m mod r[i] = 1
ans = (ans + a[i] * m * b % n) % n;
}
return (ans % n + n) % n;
}
Miller Rabin
bool millerRabin(int n) {
if (n < 3 || n % 2 == 0) return n == 2;
if (n % 3 == 0) return n == 3;
int u = n - 1, t = 0;
while (u % 2 == 0) u /= 2, ++t;
// test_time 为测试次数,建议设为不小于 8
// 的整数以保证正确率,但也不宜过大,否则会影响效率
for (int i = 0; i < test_time; ++i) {
// 0, 1, n-1 可以直接通过测试, a 取值范围 [2, n-2]
int a = rand() % (n - 3) + 2, v = quickPow(a, u, n);
if (v == 1) continue;
int s;
for (s = 0; s < t; ++s) {
if (v == n - 1) break; // 得到平凡平方根 n-1,通过此轮测试
v = (long long)v * v % n;
}
// 如果找到了非平凡平方根,则会由于无法提前 break;
// 而运行到 s == t
// 如果 Fermat 素性测试无法通过,则一直运行到 s == t
// 前 v 都不会等于 -1
if (s == t) return 0;
}
return 1;
}
Pollard_Rho
ll Pollard_Rho(ll x) {
ll t = 0;
ll c = rand() % (x - 1) + 1; // 随机常数 c
ll s = t;
int step = 0, goal = 1;
ll val = 1;
for (goal = 1;; goal <<= 1, s = t, val = 1) {
for (step = 1; step <= goal; ++step) {
t = f(t, c, x); // 迭代函数 f
val = val * abs(t - s) % x;
// 如果 val 为 0,退出重新分解
if (!val) return x;
if (step % 127 == 0) {
ll d = gcd(val, x); // 计算当前 val 和 x 的最大公约数
if (d > 1) return d; // 找到因子
}
}
ll d = gcd(val, x);
if (d > 1) return d; // 找到因子
}
}
威尔逊定理
$$ (p-1)! \equiv -1 \mod p $$
// n! mod p
int factmod(int n, int p) {
vector<int> f(p);
f[0] = 1;
for (int i = 1; i < p; i++)
f[i] = f[i - 1] * i % p;
int res = 1;
while (n > 1) {
if ((n / p) % 2)
res = p - res;
res = res * f[n % p] % p;
n /= p;
}
return res;
}
Lucas定理
Lucas定理内容如下:对于质数 p,有
$$ \binom{n}{m} \mod p = \binom{\lfloor n/p \rfloor}{\lfloor m/p \rfloor} \cdot \binom{n \mod p}{m \mod p} \mod p $$
观察上述表达式,可知 n mod p 和 m mod p 一定是小于 p 的数,可以直接求解,$\binom{\lfloor n/p \rfloor}{\lfloor m/p \rfloor}$ 可以继续用Lucas定理求解。这也就要求 p 的范围不能够太大,一般在 $10^5$ 左右。边界条件:当 m=0 的时候,返回1。
时间复杂度为 $O(f(p) + g(n)\log n)$,其中 f(n) 为预处理组合数的复杂度,g(n) 为单次求组合数的复杂度。
long long Lucas(long long n, long long m, long long p) {
if (m == 0)
return 1;
return (C(n % p, m % p, p) * Lucas(n / p, m / p, p)) % p;
}
exBSGS
给定 $a,p,b$,求满足$a^x \equiv b \mod p$的最小自然数x
#define reg register
#define EN std::puts("")
#define LL long long
inline int read() {
reg int x = 0;
reg int y = 1;
reg char c = std::getchar();
while (c < '0' || c > '9') {
if (c == '-') y = 0;
c = std::getchar();
}
while (c >= '0' && c <= '9') {
x = x * 10 + (c ^ 48);
c = std::getchar();
}
return y ? x : -x;
}
std::unordered_map<int, int> map;
int gcd(int a, int b) {
return b ? gcd(b, a % b) : a;
}
inline int BSGS(int a, int n, int p, int ad) {
map.clear();
reg int m = std::ceil(std::sqrt(p));
reg int s = 1;
for (reg int i = 0; i < m; i++, s = 1ll * s * a % p)
map[1ll * s * n % p] = i;
for (reg int i = 0, tmp = s, s = ad; i <= m; i++, s = 1ll * s * tmp % p)
if (map.find(s) != map.end())
if (1ll * i * m - map[s] >= 0)
return 1ll * i * m - map[s];
return -1;
}
inline int exBSGS(int a, int n, int p) {
a %= p;
n %= p;
if (n == 1 || p == 1) return 0;
reg int cnt = 0;
reg int d, ad = 1;
while ((d = gcd(a, p)) ^ 1) {
if (n % d) return -1;
cnt++;
n /= d;
p /= d;
ad = (1ll * ad * a / d) % p;
if (ad == n) return cnt;
}
LL ans = BSGS(a, n, p, ad);
if (ans == -1) return -1;
return ans + cnt;
}
signed main() {
int a = read(), p = read(), n = read();
while (a || p || n) {
int ans = exBSGS(a, n, p);
if (~ans) std::printf("%d\n", ans);
else std::puts("No Solution");
a = read(); p = read(); n = read();
}
return 0;
}
线性基
// 给定 n 个整数(数字可能重复),
// 求在这些数中选取任意个,使得他们的异或和最大。
using ull = unsigned long long;
constexpr int MAXN = 1e5 + 5;
ull deg(ull num, int deg) {
return num & (1ull << deg);
}
ull a[MAXN];
using std::cin;
using std::cout;
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
int n;
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> a[i];
int row = 1;
for (int col = 63; ~col && row <= n; --col) {
for (int i = row; i <= n; ++i) {
if (deg(a[i], col)) {
std::swap(a[row], a[i]);
break;
}
}
if (!deg(a[row], col)) continue;
for (int i = 1; i <= n; ++i) {
if (i == row) continue;
if (deg(a[i], col)) {
a[i] ^= a[row];
}
}
++row;
}
ull ans = 0;
for (int i = 1; i < row; ++i) {
ans ^= a[i];
}
cout << ans << '\n';
return 0;
}
高斯消元
constexpr double EPS = 1E-9;
int n;
vector<vector<double>> a(n, vector<double>(n));
double det = 1;
for (int i = 0; i < n; ++i) {
int k = i;
for (int j = i + 1; j < n; ++j)
if (abs(a[j][i]) > abs(a[k][i])) k = j;
if (abs(a[k][i]) < EPS) {
det = 0;
break;
}
swap(a[i], a[k]);
if (i != k) det = -det;
det *= a[i][i];
for (int j = i + 1; j < n; ++j)
a[i][j] /= a[i][i];
for (int j = 0; j < n; ++j)
if (j != i && abs(a[j][i]) > EPS)
for (int k = i + 1; k < n; ++k)
a[j][k] -= a[i][k] * a[j][i];
}
cout << det;
博弈论
Sprague-Grundy 定理 (SG定理)
定义 mex 函数的值为不属于集合 S 中的最小非负整数,即:
$$\text{mex}(S) = \min{x} \quad (x \notin S, x \in N)$$
例如:
$$\operatorname{mex}({0,2,4}) = 1, \quad \operatorname{mex}({1,2}) = 0$$
对于状态 x 和它的所有 k 个后继状态 $y_1, y_2, \ldots, y_k$,定义 SG 函数:
$$\text{SG}(x) = \text{mex}{\text{SG}(y_1), \text{SG}(y_2), \ldots, \text{SG}(y_k)}$$
而对于由 n 个有向图游戏组成的组合游戏,设它们的起点分别为 $s_1, s_2, \ldots, s_n$,则有定理:当且仅当:
$$SG(s_1) \oplus SG(s_2) \oplus \ldots \oplus SG(s_n) \neq 0$$
时,这个游戏是先手必胜的。同时,这是这一个组合游戏的游戏状态 x 的 SG 值。
字符串
双值hash
using ull = unsigned long long;
ull base = 131;
ull mod1 = 212370440130137957, mod2 = 1e9 + 7;
ull get_hash1(std::string s) {
int len = s.size();
ull ans = 0;
for (int i = 0; i < len; i++)
ans = (ans * base + (ull)s[i]) % mod1;
return ans;
}
ull get_hash2(std::string s) {
int len = s.size();
ull ans = 0;
for (int i = 0; i < len; i++)
ans = (ans * base + (ull)s[i]) % mod2;
return ans;
}
bool cmp(const std::string s, const std::string t) {
bool f1 = get_hash1(s) != get_hash1(t);
bool f2 = get_hash2(s) != get_hash2(t);
return f1 || f2;
}
KMP
vector<int> prefix_function(string s) {
int n = (int)s.length();
vector<int> pi(n);
for (int i = 1; i < n; i++) {
int j = pi[i - 1];
while (j > 0 && s[i] != s[j]) j = pi[j - 1];
if (s[i] == s[j]) j++;
pi[i] = j;
}
return pi;
}
vector<int> find_occurrences(string text, string pattern) {
string cur = pattern + '#' + text;
int sz1 = text.size(), sz2 = pattern.size();
vector<int> v;
vector<int> lps = prefix_function(cur);
for (int i = sz2 + 1; i <= sz1 + sz2; i++) {
if (lps[i] == sz2)
v.push_back(i - 2 * sz2);
}
return v;
}
// 统计每个前缀的出现次数
vector<int> ans(n + 1);
for (int i = 0; i < n; i++)
ans[pi[i]]++;
for (int i = n - 1; i > 0; i--)
ans[pi[i - 1]] += ans[i];
for (int i = 0; i <= n; i++)
ans[i]++;
AC自动机
constexpr int N = 2e5 + 6;
constexpr int LEN = 2e6 + 6;
constexpr int SIZE = 2e5 + 6;
int n;
namespace AC {
struct Node {
int son[26];
int ans;
int fail;
int idx;
void init() {
memset(son, 0, sizeof(son));
ans = idx = 0;
}
} tr[SIZE];
int tot;
int ans[N], pidx;
vector<int> g[SIZE]; // fail 树
void init() {
tot = pidx = 0;
tr[0].init();
}
void insert(char s[], int &idx) {
int u = 0;
for (int i = 1; s[i]; i++) {
int &son = tr[u].son[s[i] - 'a'];
if (!son) son = ++tot, tr[son].init();
u = son;
}
// 由于有可能出现相同的模式串,需要将相同的映射到同一个编号
if (!tr[u].idx) tr[u].idx = ++pidx; // 第一次出现,新增编号
idx = tr[u].idx; // 这个模式串的编号对应这个结点的编号
}
void build() {
queue<int> q;
for (int i = 0; i < 26; i++)
if (tr[0].son[i]) {
q.push(tr[0].son[i]);
g[0].push_back(tr[0].son[i]); // 不要忘记这里的 fail
}
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = 0; i < 26; i++) {
if (tr[u].son[i]) {
tr[tr[u].son[i]].fail = tr[tr[u].fail].son[i];
g[tr[tr[u].fail].son[i]].push_back(tr[u].son[i]);
q.push(tr[u].son[i]);
} else
tr[u].son[i] = tr[tr[u].fail].son[i];
}
}
}
void query(char t[]) {
int u = 0;
for (int i = 1; t[i]; i++) {
u = tr[u].son[t[i] - 'a'];
tr[u].ans++;
}
}
void dfs(int u) {
for (int v : g[u]) {
dfs(v);
tr[u].ans += tr[v].ans;
}
ans[tr[u].idx] = tr[u].ans;
}
} // namespace AC
char s[LEN];
int idx[N];
int main() {
AC::init();
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%s", s + 1);
AC::insert(s, idx[i]);
AC::ans[i] = 0;
}
AC::build();
scanf("%s", s + 1);
AC::query(s);
AC::dfs(0);
for (int i = 1; i <= n; i++) {
printf("%d\n", AC::ans[idx[i]]);
}
return 0;
}
最小表示法
字符串S的最小表示为与S循环同构的所有字符串中字典序最小的字符串。
int k = 0, i = 0, j = 1;
while (k < n && i < n && j < n) {
if (sec[(i + k) % n] == sec[(j + k) % n]) {
k++;
} else {
sec[(i + k) % n] > sec[(j + k) % n] ? i = i + k + 1 : j = j + k + 1;
if (i == j) i++;
k = 0;
}
}
i = min(i, j);
Manacher
vector<int> d1(n); // 存储奇数长度回文的半径
for (int i = 0, l = 0, r = -1; i < n; i++) {
int k = (i > r) ? 1 : min(d1[l + r - i], r - i + 1);
while (0 <= i - k && i + k < n && s[i - k] == s[i + k]) {
k++;
}
d1[i] = k--;
if (i + k > r) {
l = i - k;
r = i + k;
}
}
vector<int> d2(n); // 存储偶数长度回文的半径
for (int i = 0, l = 0, r = -1; i < n; i++) {
int k = (i > r) ? 0 : min(d2[l + r - i + 1], r - i + 1);
while (0 <= i - k - 1 && i + k < n && s[i - k - 1] == s[i + k]) {
k++;
}
d2[i] = k--;
if (i + k > r) {
l = i - k - 1;
r = i + k;
}
}
Misc
线段树分治
例:你需要维护一个 n 个点 m 条边的无向图。第 i 条边为 (x_i,y_i),出现的时刻为 [l_i,r_i),其余时刻消失。对于每一个时刻,若此时该图为二分图,输出 Yes
,否则输出 No
。
#define ls (i << 1)
#define rs (i << 1 | 1)
#define mid ((l + r) >> 1)
int n, m, k;
constexpr int N = 2e5 + 5;
int fa[N << 1], siz[N << 1];
struct UndoObject {
int pos, val;
UndoObject(int p, int v) : pos(p), val(v) {}
};
stack<UndoObject> undo_sz, undo_fa;
int find(int x) {
if (fa[x] == x)
return x;
else
return find(fa[x]);
}
void merge(int u, int v) {
int x = find(u), y = find(v);
if (x == y) return;
if (siz[x] < siz[y]) {
swap(x, y);
}
undo_sz.push(UndoObject(x, siz[x]));
siz[x] += siz[y];
undo_fa.push(UndoObject(y, fa[y]));
fa[y] = x;
}
void undo() {
fa[undo_fa.top().pos] = undo_fa.top().val;
undo_fa.pop();
siz[undo_sz.top().pos] = undo_sz.top().val;
undo_sz.pop();
}
vector<int> tree[N << 2];
void update(int ql, int qr, int v, int i, int l, int r) {
if (ql <= l && r <= qr) {
tree[i].push_back(v);
return;
}
if (ql <= mid) update(ql, qr, v, ls, l, mid);
if (qr > mid) update(ql, qr, v, rs, mid + 1, r);
}
struct edge {
int u, v;
} g[N];
vector<bool> ret;
void solve(int i, int l, int r) {
auto level = undo_fa.size();
bool ans = true;
for (int u : tree[i]) {
int a = find(g[u].u);
int b = find(g[u].v);
if (a == b) {
for (int k = l; k <= r; k++) {
ret.push_back(false);
}
ans = false;
break;
}
merge(g[u].u, g[u].v + n);
merge(g[u].v, g[u].u + n);
}
if (ans) {
if (l == r) {
ret.push_back(true);
} else {
solve(ls, l, mid);
solve(rs, mid + 1, r);
}
}
while (undo_fa.size() > level) {
undo();
}
}
signed main() {
cin >> n >> m >> k;
for (int i = 1; i <= (n << 1); i++) {
fa[i] = i;
siz[i] = 1;
}
for (int i = 1, l, r; i <= m; i++) {
cin >> g[i].u >> g[i].v >> l >> r;
update(l + 1, r, i, 1, 1, k);
}
solve(1, 1, k);
for (bool i : ret) {
cout << (i ? "Yes" : "No") << '\n';
}
return 0;
}
莫队
将大小为n的序列分为$\sqrt n$个块,从1到$\sqrt n$编号,然后根据这个对查询区间进行排序。把查询区间按照左端点所在块的序号排个序,如果左端点所在块相同,再按右端点排序
int cmp(query a, query b)
{
return belong[a.l] == belong[b.l] ?
a.r < b.r : belong[a.l] < belong[b.l];
}
// 奇偶性排序
int cmp(query a, query b) {
return (belong[a.l] ^ belong[b.l]) ?
belong[a.l] < belong[b.l] :
((belong[a.l] & 1) ? a.r < b.r :
a.r > b.r); }
例:
#define maxn 1010000
#define maxb 1010
int aa[maxn], cnt[maxn], belong[maxn];
int n, m, size, bnum, now, ans[maxn];
struct query {
int l, r, id;
} q[maxn];
int cmp(query a, query b) {
return (belong[a.l] ^ belong[b.l]) ? belong[a.l] < belong[b.l] :
((belong[a.l] & 1) ? a.r < b.r : a.r > b.r);
}
#define isdigit(x) ((x) >= '0' && (x) <= '9')
int read() {
int res = 0;
char c = getchar();
while (!isdigit(c)) c = getchar();
while (isdigit(c)) res = (res << 1) + (res << 3) + c - 48, c = getchar();
return res;
}
void printi(int x) {
if (x / 10) printi(x / 10);
putchar(x % 10 + '0');
}
int main() {
scanf("%d", &n);
size = sqrt(n);
bnum = ceil((double)n / size);
for (int i = 1; i <= bnum; ++i)
for (int j = (i - 1) * size + 1; j <= i * size; ++j) {
belong[j] = i;
}
for (int i = 1; i <= n; ++i) aa[i] = read();
m = read();
for (int i = 1; i <= m; ++i) {
q[i].l = read(), q[i].r = read();
q[i].id = i;
}
sort(q + 1, q + m + 1, cmp);
int l = 1, r = 0;
for (int i = 1; i <= m; ++i) {
int ql = q[i].l, qr = q[i].r;
while (l < ql) now -= !--cnt[aa[l++]];
while (l > ql) now += !cnt[aa[--l]]++;
while (r < qr) now += !cnt[aa[++r]]++;
while (r > qr) now -= !--cnt[aa[r--]];
ans[q[i].id] = now;
}
for (int i = 1; i <= m; ++i) printi(ans[i]), putchar('\n');
return 0;
}
带修莫队
莫队算法是离线算法,不支持修改,强制在线需要另寻他法。的确,遇到强制在线的题目莫队基本上萎了,但是对于某些允许离线的带修改区间查询来说,莫队还是能大展拳脚的。做法就是把莫队直接加上一维,变为带修莫队。
那么加上一维什么呢?具体怎么实现?我们的做法是把修改操作编号,称为"时间戳",而查询操作的时间戳沿用之前最近的修改操作的时间戳。跑主算法时定义当前时间戳为 tt ,对于每个查询操作,如果当前时间戳相对太大了,说明已进行的修改操作比要求的多,就把之前改的改回来,反之往后改。只有当当前区间和查询区间左右端点、时间戳均重合时,才认定区间完全重合,此时的答案才是本次查询的最终答案。
通俗地讲,就是再弄一指针,在修改操作上跳来跳去,如果当前修改多了就改回来,改少了就改过去,直到次数恰当为止。
int cmp(query a, query b) {
return (belong[a.l] ^ belong[b.l]) ?
belong[a.l] < belong[b.l]
: ((belong[a.r] ^ belong[b.r])
? belong[a.r] < belong[b.r]
: a.time < b.time);
}
树上莫队
具体做法:欧拉序,设每个点的编号a首次出现的位置first[a]first[a],最后出现的位置为last[a],那么对于路径x→y,设first[x]<=first[y](不满足则swap
,这个操作的意义在于,如果x、y在一条链上,则x一定是y的祖先或等于yy),如果lca(x,y)=x,则直接把 [first[x],first[y]] 的区间扯过来用,反之使用[last[x],first[y]]区间(为什么不用[first[x],first[y]]?因为(first[x],last[x])不会在路径上,根据性质,里面的编号都会出现两次,考虑了等于没考虑),但这个区间内不包含x和y的最近公共祖先,查询的时候加上即可。
LCT
#include <cstdio>
#include <algorithm>
#define Fa(x) (node[x].fa)
#define Ls(x) (node[x].ch[0])
#define Rs(x) (node[x].ch[1])
const int N = 3e4 + 10;
struct Splay {
int fa, ch[2], w, sum, revFlag;
} node[N];
inline void pushup(int x) {
node[x].sum = node[Ls(x)].sum + node[Rs(x)].sum + node[x].w;
}
inline void reverse(int x) {
std::swap(Ls(x), Rs(x));
node[x].revFlag ^= 1;
}
inline void pushdown(int x) {
if (!node[x].revFlag) return;
if (Ls(x)) reverse(Ls(x));
if (Rs(x)) reverse(Rs(x));
node[x].revFlag = 0;
}
inline int get(int x) {
return Rs(Fa(x)) == x;
}
inline int nRoot(int x) {
return Rs(Fa(x)) == x || Ls(Fa(x)) == x;
}
inline void rotate(int x) {
int fa = Fa(x), gf = Fa(fa), d = get(x), dd = get(fa);
if (nRoot(fa)) node[gf].ch[dd] = x;
node[fa].ch[d] = node[x].ch[d ^ 1];
Fa(node[x].ch[d ^ 1]) = fa;
node[x].ch[d ^ 1] = fa;
Fa(fa) = x;
Fa(x) = gf;
pushup(fa);
pushup(x);
}
int st[N], tp;
inline void splay(int x) {
int y = st[tp = 1] = x;
while (nRoot(y)) st[++tp] = y = Fa(y);
while (tp) pushdown(st[tp--]);
for (; nRoot(x); rotate(x)) if (nRoot(Fa(x)))
rotate(get(Fa(x)) == get(x) ? Fa(x) : x);
pushup(x);
}
inline void access(int x) {
int y = 0;
while (x) {
splay(x);
Rs(x) = y;
pushup(x);
x = Fa(y = x);
}
}
inline void makeroot(int x) {
access(x);
splay(x);
reverse(x);
}
inline void split(int x, int y) {
makeroot(x);
access(y);
splay(y);
}
inline void link(int x, int y) {
makeroot(x);
Fa(x) = y;
}
inline void cut(int x, int y) {
split(x, y);
Fa(x) = Ls(y) = 0;
}
inline int findRoot(int x) {
access(x);
splay(x);
while (Ls(x)) x = Ls(x), pushdown(x);
splay(x);
return x;
}
/*
- `bridge u v`:询问结点u与结点v是否连通。如果是则输出 `no`;否则输出 `yes`,并且在结点 uu 和结点 vv 之间连一条无向边。
- `penguins u x`:将结点 uu 对应的权值 wuwu 修改为 xx。
- `excursion u v`:如果结点 uu 和结点 vv 不连通,则输出 `impossible`。否则输出结点 uu 到结点 vv 的路径上的点对应的权值的和。
*/
int main() {
int n, m;
char s[15];
scanf("%d", &n);
for (int i = 1; i <= n; ++i) scanf("%d", &node[i].w);
scanf("%d", &m);
int u, v;
for (int i = 1; i <= m; ++i) {
scanf("%s%d%d", s + 1, &u, &v);
if (s[1] == 'e') {
if (findRoot(u) != findRoot(v)) printf("impossible\n");
else split(u, v), printf("%d\n", node[v].sum);
} else if (s[1] == 'p') {
splay(u);
node[u].w = v;
pushup(u);
} else {
if (findRoot(u) == findRoot(v)) printf("no\n");
else printf("yes\n"), link(u, v);
}
}
return 0;
}
珂朵莉树
typedef long long ll;
const ll MOD = 1000000007;
const ll MAXN = 100005;
struct Node {
ll l, r; // l和r表示这一段的起点和终点
mutable ll v; // v表示这一段上所有元素相同的值是多少
Node(ll l, ll r = 0, ll v = 0) : l(l), r(r), v(v) {}
bool operator<(const Node &a) const {
return l < a.l; // 规定按照每段的左端点排序
}
};
ll n, m, seed, vmax, a[MAXN];
set<Node> s;
// 以pos去做切割,找到一个包含pos的区间,把它分成[l,pos-1],[pos,r]两半
set<Node>::iterator split(int pos) {
set<Node>::iterator it = s.lower_bound(Node(pos));
if (it != s.end() && it->l == pos) {
return it;
}
it--;
if (it->r < pos) return s.end();
ll l = it->l;
ll r = it->r;
ll v = it->v;
s.erase(it);
s.insert(Node(l, pos - 1, v));
return s.insert(Node(pos, r, v)).first;
}
void add(ll l, ll r, ll x) {
set<Node>::iterator itr = split(r + 1), itl = split(l);
for (set<Node>::iterator it = itl; it != itr; ++it) {
it->v += x;
}
}
void assign(ll l, ll r, ll x) {
set<Node>::iterator itr = split(r + 1), itl = split(l);
s.erase(itl, itr);
s.insert(Node(l, r, x));
}
struct Rank {
ll num, cnt;
bool operator<(const Rank &a) const {
return num < a.num;
}
Rank(ll num, ll cnt) : num(num), cnt(cnt) {}
};
ll rnk(ll l, ll r, ll x) {
set<Node>::iterator itr = split(r + 1), itl = split(l);
vector<Rank> v;
for (set<Node>::iterator i = itl; i != itr; ++i) {
v.push_back(Rank(i->v, i->r - i->l + 1));
}
sort(v.begin(), v.end());
int i;
for (i = 0; i < v.size(); ++i) {
if (v[i].cnt < x) {
x -= v[i].cnt;
} else {
break;
}
}
return v[i].num;
}
ll ksm(ll x, ll y, ll p) {
ll r = 1;
ll base = x % p;
while (y) {
if (y & 1) {
r = r * base % p;
}
base = base * base % p;
y >>= 1;
}
return r;
}
ll calP(ll l, ll r, ll x, ll y) {
set<Node>::iterator itr = split(r + 1), itl = split(l);
ll ans = 0;
for (set<Node>::iterator i = itl; i != itr; ++i) {
ans = (ans + ksm(i->v, x, y) * (i->r - i->l + 1) % y) % y;
}
return ans;
}
ll rnd() {
ll ret = seed;
seed = (seed * 7 + 13) % MOD;
return ret;
}
int main() {
cin >> n >> m >> seed >> vmax;
for (int i = 1; i <= n; ++i) {
a[i] = (rnd() % vmax) + 1;
s.insert(Node(i, i, a[i]));
}
for (int i = 1; i <= m; ++i) {
ll op, l, r, x, y;
op = (rnd() % 4) + 1;
l = (rnd() % n) + 1;
r = (rnd() % n) + 1;
if (l > r) swap(l, r);
if (op == 3) {
x = (rnd() % (r - l + 1)) + 1;
} else {
x = (rnd() % vmax) + 1;
}
if (op == 4) {
y = (rnd() % vmax) + 1;
}
if (op == 1) {
add(l, r, x);
} else if (op == 2) {
assign(l, r, x);
} else if (op == 3) {
cout << rnk(l, r, x) << endl;
} else {
cout << calP(l, r, x, y) << endl;
}
}
return 0;
}
三维偏序
constexpr int MAXN = 1e5 + 10;
constexpr int MAXK = 2e5 + 10;
int n, k;
struct Element {
int a, b, c;
int cnt;
int res;
bool operator!=(Element other) const {
if (a != other.a) return true;
if (b != other.b) return true;
if (c != other.c) return true;
return false;
}
};
Element e[MAXN];
Element ue[MAXN];
int m, t;
int res[MAXN];
struct BinaryIndexedTree {
int node[MAXK];
int lowbit(int x) { return x & -x; }
void Add(int pos, int val) {
while (pos <= k) {
node[pos] += val;
pos += lowbit(pos);
}
return;
}
int Ask(int pos) {
int res = 0;
while (pos) {
res += node[pos];
pos -= lowbit(pos);
}
return res;
}
} BIT;
bool cmpA(Element x, Element y) {
if (x.a != y.a) return x.a < y.a;
if (x.b != y.b) return x.b < y.b;
return x.c < y.c;
}
bool cmpB(Element x, Element y) {
if (x.b != y.b) return x.b < y.b;
return x.c < y.c;
}
void CDQ(int l, int r) {
if (l == r) return;
int mid = (l + r) / 2;
CDQ(l, mid);
CDQ(mid + 1, r);
std::sort(ue + l, ue + mid + 1, cmpB);
std::sort(ue + mid + 1, ue + r + 1, cmpB);
int i = l;
int j = mid + 1;
while (j <= r) {
while (i <= mid && ue[i].b <= ue[j].b) {
BIT.Add(ue[i].c, ue[i].cnt);
i++;
}
ue[j].res += BIT.Ask(ue[j].c);
j++;
}
for (int k = l; k < i; k++) BIT.Add(ue[k].c, -ue[k].cnt);
return;
}
using std::cin;
using std::cout;
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
cin >> n >> k;
for (int i = 1; i <= n; i++) cin >> e[i].a >> e[i].b >> e[i].c;
std::sort(e + 1, e + n + 1, cmpA);
t = 1;
for (int i = 1; i <= n; i++) {
if (i == n || e[i] != e[i + 1]) {
m++;
ue[m].a = e[i].a;
ue[m].b = e[i].b;
ue[m].c = e[i].c;
ue[m].cnt = t;
t = 0;
}
t++;
}
CDQ(1, m);
for (int i = 1; i <= m; i++) res[ue[i].res + ue[i].cnt - 1] += ue[i].cnt;
for (int i = 0; i < n; i++) cout << res[i] << '\n';
return 0;
}
CDQ
CDQ分治优化1D/1D动态规划的转移
1D/1D动态规划指的是一类特定的DP问题,该类题目的特征是DP数组是一维的,转移是 $O(n)$ 的。如果条件良好的话,有时可以通过CDQ分治来把它们的时间复杂度由 $O(n^2)$ 降至 $O(n\log^2n)$。
例如,给定一个序列,每个元素有两个属性 $a, b_i$,我们希望计算一个 DP式子的值,它的转移方程如下:
$$dp_i = 1 + \max_{j=1}^{i-1} dp_j [a_j < a_i][b_j < b_i]$$
这是一个二维最长上升子序列的DP方程,即只有 $j < i, a_j < a_i, b_j < b_i$ 的点 j 可以更新点 i 的 DP值。
直接转移显然是 $O(n^2)$ 的。以下是使用 CDQ分治优化转移过程的讲解。
我们发现 $dp_j$ 转移到 $dp_i$ 这种转移关系也是一种点对间的关系,所以我们用类似 CDQ分治处理点对关系的方式来处理它。
这个转移过程相对来讲比较套路。假设现在正在处理的区间是 $(l, r)$,算法流程大致如下:
- 如果 $l = r$,说明 $dp_r$ 值已经被计算好了。直接令 $dp_r++$ 然后返回即可;
- 递归使用 solve(l, mid);
- 处理所有 $l \leq j \leq mid$,mid + 1 $\leq i \leq r$ 的转移关系;
- 递归使用 solve(mid+1, r)。
第三步的做法与CDQ分治求三维偏序差不多。处理 $l \leq j \leq m$ id,mid + 1 $\leq i \leq r$ 的转移关系的时候,我们会发现已经不用管 $j < i$ 这个限制条件了。因此,我们依然先将所有的点 i 和点 j 按 a 值进行排序处理,然后用双指针的方式将 j 点插入到树状数组里,最后查一下前缀最大值更新一下 $dp_i$ 就可以了。
将动态问题转化为静态问题
前两种情况使用CDQ分治的目的是将序列折半之后递归处理点对间的关系,来获得良好的复杂度。不过在本节中,折半的不是一般的序列,而是时间序列。
它适用于一些「需要支持做xxx修改然后做xxx询问」的数据结构题。该类题目有两个特点:
- 如果把询问离线,所有操作会按照时间自然地排成一个序列。
- 每一个修改均与之后的询问操作息息相关。而这样的「修改-询问」关系一共会有 $O(n^2)$ 对。
我们可以使用CDQ分治对于这个操作序列进行分治,处理修改和询问之间的关系。
与处理点对关系的CDQ分治类似,假设正在分治的序列是 (l,r),我们先递归地处理 (l,mid) 和 (mid,r) 之间的修改-询问关系,再处理所有 $l \leq i \leq mid, \text{ mid} + 1 \leq j \leq r$ 的修改-询问关系,其中i是一个修改,j是一个询问。(mid,r) 之间的修改-询问关系,再处理所有 $l \leq i \leq mid, \text{ mid} + 1 \leq j \leq r$ 的修改-询问关系,
注意,如果各个修改之间是独立的话,我们无需处理 $l \leq i \leq mid$ 和 $mid + 1 \leq j \leq r$,以及 solve(l,mid)
和 solve(mid+1,r)
之间的时序关系(比如普通的加减法问题)。但是如果各个修改之间并不独立(比如说赋值操作),做完这个修改后,序列长什么样可能依赖于之前的序列。此时处理所有跨越mid的修改-询问关系的步骤就必须放在 solve(l,mid)
和 solve(mid+1,r)
之间。理由和CDQ分治优化1D/1D动态规划的原因是一样的:按照中序遍历序进行分治才能保证每一个修改都是严格按照时间顺序执行的。
例:维护一个长为 n 的序列 $a_i$,有 m 次操作。
- 将区间 $[l, r]$ 的值修改为 x;
- 查询区间 $[l, r]$ 出现了多少种不同的数,也就是说同一个数出现多次只算一个。
解题思路:
一句话题意:区间赋值区间数颜色。
维护一下每个位置左侧第一个同色点的位置,记为 $pre_i$,此时区间数颜色就被转化为了一个经典的二维数点问题。
通过将连续的一段颜色看成一个点的方式,可以证明 pre 的变化量是 $O(n+m)$ 的,即单次操作仅仅引起 $O(1)$ 的 pre 值变化,那么我们可以用 CDQ 分治来解决动态的单点加矩形求和问题。
pre 数组的具体变化可以使用 std::set 来进行处理。这个用 set 维护连续的区间的技巧也被称之为 old driver tree。
#include <algorithm>
#include <iostream>
#include <map>
#include <set>
using namespace std;
typedef long long ll;
const ll MOD = 1000000007;
const ll MAXN = 100005;
struct Node {
ll l, r; // l和r表示这一段的起点和终点
mutable ll v; // v表示这一段上所有元素相同的值是多少
Node(ll l, ll r = 0, ll v = 0) : l(l), r(r), v(v) {}
bool operator<(const Node &a) const {
return l < a.l; // 规定按照每段的左端点排序
}
};
ll n, m, seed, vmax, a[MAXN];
set<Node> s;
// 以pos去做切割,找到一个包含pos的区间,把它分成[l,pos-1],[pos,r]两半
set<Node>::iterator split(int pos) {
set<Node>::iterator it = s.lower_bound(Node(pos));
if (it != s.end() && it->l == pos) {
return it;
}
it--;
if (it->r < pos) return s.end();
ll l = it->l;
ll r = it->r;
ll v = it->v;
s.erase(it);
s.insert(Node(l, pos - 1, v));
return s.insert(Node(pos, r, v)).first;
}
void add(ll l, ll r, ll x) {
set<Node>::iterator itr = split(r + 1), itl = split(l);
for (set<Node>::iterator it = itl; it != itr; ++it) {
it->v += x;
}
}
void assign(ll l, ll r, ll x) {
set<Node>::iterator itr = split(r + 1), itl = split(l);
s.erase(itl, itr);
s.insert(Node(l, r, x));
}
struct Rank {
ll num, cnt;
bool operator<(const Rank &a) const {
return num < a.num;
}
Rank(ll num, ll cnt) : num(num), cnt(cnt) {}
};
ll rnk(ll l, ll r, ll x) {
set<Node>::iterator itr = split(r + 1), itl = split(l);
vector<Rank> v;
for (set<Node>::iterator i = itl; i != itr; ++i) {
v.push_back(Rank(i->v, i->r - i->l + 1));
}
sort(v.begin(), v.end());
int i;
for (i = 0; i < v.size(); ++i) {
if (v[i].cnt < x) {
x -= v[i].cnt;
} else {
break;
}
}
return v[i].num;
}
ll ksm(ll x, ll y, ll p) {
ll r = 1;
ll base = x % p;
while (y) {
if (y & 1) {
r = r * base % p;
}
base = base * base % p;
y >>= 1;
}
return r;
}
ll calP(ll l, ll r, ll x, ll y) {
set<Node>::iterator itr = split(r + 1), itl = split(l);
ll ans = 0;
for (set<Node>::iterator i = itl; i != itr; ++i) {
ans = (ans + ksm(i->v, x, y) * (i->r - i->l + 1) % y) % y;
}
return ans;
}
ll rnd() {
ll ret = seed;
seed = (seed * 7 + 13) % MOD;
return ret;
}
int main() {
cin >> n >> m >> seed >> vmax;
for (int i = 1; i <= n; ++i) {
a[i] = (rnd() % vmax) + 1;
s.insert(Node(i, i, a[i]));
}
for (int i = 1; i <= m; ++i) {
ll op, l, r, x, y;
op = (rnd() % 4) + 1;
l = (rnd() % n) + 1;
r = (rnd() % n) + 1;
if (l > r) swap(l, r);
if (op == 3) {
x = (rnd() % (r - l + 1)) + 1;
} else {
x = (rnd() % vmax) + 1;
}
if (op == 4) {
y = (rnd() % vmax) + 1;
}
if (op == 1) {
add(l, r, x);
} else if (op == 2) {
assign(l, r, x);
} else if (op == 3) {
cout << rnk(l, r, x) << endl;
} else {
cout << calP(l, r, x, y) << endl;
}
}
return 0;
}
整体二分
记 $[l, r]$ 为答案的值域, $[L, R]$ 为答案的定义域。(也就是说求答案时仅考虑下标在区间 $[L, R]$ 内的操作和询问,这其中询问的答案在 $[l, r]$ 内)
- 我们首先把所有操作按时间顺序存入数组中, 然后开始分治。
- 在每一层分治中, 利用数据结构(常见的是树状数组)统计当前查询的答案和 mid 之间的关系。
- 根据查询出来的答案和 mid 间的关系(小于等于 mid 和大于 mid)将当前处理的操作序列分为 q1 和 q2 两份,并分别递归处理。
当 $l = r$ 时,找到答案,记录答案并返回即可。
需要注意的是,在整体二分过程中,若当前处理的值域为 $[l, r]$,则此时最终答案范围不在 $[l, r]$ 的询问会在其他时候处理。
例1:全局第k小
int check(int l, int r) {
int res = 0;
for (int i = 1; i <= n; i++) {
if (l <= a[i] && a[i] <= r) res++;
}
return res;
}
void solve(int l, int r, vector<Query> q) {
if (l > r) return;
int m = (l + r) / 2;
if (l == r) {
for (auto& qi : q) ans[qi.id] = l;
return;
}
vector<Query> q1, q2;
int t = check(l, m);
for (auto& qi : q) {
if (qi.k <= t) {
q1.push_back(qi);
} else {
qi.k -= t;
q2.push_back(qi);
}
}
solve(l, m, q1);
solve(m + 1, r, q2);
}
例2:区间第k小
struct Num {
int p, x;
}; // 位于数列中第 p 项的数的值为 x
struct Query {
int l, r, k, id;
}; // 一个编号为 id, 询问 [l,r] 中第 k 小数的询问
int ans[N];
void add(int p, int x); // 树状数组, 在 p 位置加上 x
int query(int p); // 树状数组, 求 [1,p] 的和
void clear(); // 树状数组, 清空
void solve(int l, int r, vector<Num> a, vector<Query> q) {
int m = (l + r) / 2;
if (l == r) {
for (auto& qi : q) ans[qi.id] = l;
return;
}
vector<Num> a1, a2;
vector<Query> q1, q2;
for (auto& num : a)
if (num.x <= m)
a1.push_back(num), add(num.p, 1);
else
a2.push_back(num);
for (auto& qi : q) {
int t = query(qi.r) - query(qi.l - 1);
if (qi.k <= t)
q1.push_back(qi);
else
q2.push_back({qi.l, qi.r, qi.k - t, qi.id});
}
clear();
solve(l, m, a1, q1);
solve(m + 1, r, a2, q2);
}
例3:带修区间第k小
struct Opt {
int x, y, k, type, id;
// 对于询问, type = 1, x, y 表示区间左右边界, k 表示询问第 k 小
// 对于修改, type = 0, x 表示修改位置, y 表示修改后的值,
// k 表示当前操作是插入(1)还是擦除(-1), 更新树状数组时使用.
// id 记录每个操作原先的编号, 因二分过程中操作顺序会被打散
};
Opt q[N], q1[N], q2[N];
// q 为所有操作,
// 二分过程中, 分到左边的操作存到 q1 中, 分到右边的操作存到 q2 中.
int ans[N];
void add(int p, int x);
int query(int p); // 树状数组函数, 含义见题3
void solve(int l, int r, int L, int R) {
// 当前的值域范围为 [l,r], 处理的操作的区间为 [L,R]
if (l > r || L > R) return;
int cnt1 = 0, cnt2 = 0, m = (l + r) / 2;
// cnt1, cnt2 分别为分到左边, 分到右边的操作数
if (l == r) {
for (int i = L; i <= R; i++)
if (q[i].type == 1) ans[q[i].id] = l;
return;
}
for (int i = L; i <= R; i++)
if (q[i].type == 1) { // 是询问: 进行分类
int t = query(q[i].y) - query(q[i].x - 1);
if (q[i].k <= t)
q1[++cnt1] = q[i];
else
q[i].k -= t, q2[++cnt2] = q[i];
} else
// 是修改: 更新树状数组 & 分类
if (q[i].y <= m)
add(q[i].x, q[i].k), q1[++cnt1] = q[i];
else
q2[++cnt2] = q[i];
for (int i = 1; i <= cnt1; i++)
if (q1[i].type == 0) add(q1[i].x, -q1[i].k); // 清空树状数组
for (int i = 1; i <= cnt1; i++) q[L + i - 1] = q1[i];
for (int i = 1; i <= cnt2; i++)
q[L + cnt1 + i - 1] = q2[i]; // 将临时数组中的元素合并回原数组
solve(l, m, L, L + cnt1 - 1), solve(m + 1, r, L + cnt1, R);
return;
}
附录
AT1219
/*
回滚莫队
要点:左端点分块,右端点递增,初始两个指针指向块的末端,特判左右断点在同一块内的情况!
*/
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define pb push_back
const int MAXN=int(1e5+5);
const int MAXB=int(1e5+5);
int n,m,bnum;
ll a[MAXN];
int col[MAXN],cnt[MAXN],cnt2[MAXN];
vector<ll> alls;
int belong[MAXN];
int st[MAXB],ed[MAXB];
int l,r;
ll ans[MAXN],now;
struct query {
int l,r,id;
bool operator < (const query &W) const {
return (belong[l]^belong[W.l])?belong[l]<belong[W.l]:r<W.r;
}
}Q[MAXN];
int find(ll x) {
int l=0,r=alls.size()-1;
while(l<r) {
int mid=(l+r+1)/2;
if(alls[mid]<=x)
l=mid;
else
r=mid-1;
}
return l+1;
}
void Init_col() {
sort(alls.begin(),alls.end());
alls.erase(unique(alls.begin(),alls.end()),alls.end());
for(int i=1;i<=n;i++)
col[i]=find(a[i]);
}
void Init_belong() {
int size=sqrt(n);
bnum=ceil(n/size);
for(int i=1;i<=n;i++) {
st[i]=(i-1)*size+1,ed[i]=min(n,i*size);
for(int j=st[i];j<=ed[i];j++)
belong[j]=i;
}
}
void Add(int p) {
cnt[col[p]]++;
now=max(now,1ll*cnt[col[p]]*a[p]);
}
void Del(int p) {
cnt[col[p]]--;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) {
scanf("%lld",&a[i]);
alls.pb(a[i]);
}
Init_col();
Init_belong();
for(int i=1;i<=m;i++) {
int ql,qr;
scanf("%d%d",&ql,&qr);
Q[i]={ql,qr,i};
}
sort(Q+1,Q+m+1);
int i=1;
for(int k=1;k<=bnum;k++) {
l=ed[k],r=ed[k];
now=0;
memset(cnt,0,sizeof(cnt));
Add(l);
while(belong[Q[i].l]==k) {
int ql=Q[i].l,qr=Q[i].r;
if(belong[ql]==belong[qr]) {
ll tmp=0;
for(int j=ql;j<=qr;j++) cnt2[col[j]]=0;
for(int j=ql;j<=qr;j++) {
cnt2[col[j]]++;
tmp=max(tmp,a[j]*(1ll*cnt2[col[j]]));
}
ans[Q[i].id]=tmp;
i++;
continue;
}
while(r<qr)
Add(++r);
ll tmp=now;
while(l>ql)
Add(--l);
ans[Q[i].id]=now;
while(l<ed[k]+1)
Del(l++);
now=tmp;
i++;
}
}
for(int i=1;i<=m;i++)
printf("%lld\n",ans[i]);
}
luogu_1903
// 带修莫队
#include<bits/stdc++.h>
using namespace std;
const int MAXN=int(1e6+5);
int n,m,qm,cm;
int a[MAXN],ans[MAXN],belong[MAXN];
int cnt[MAXN],sum;
int l,r,tag;
struct query {
int l,r,t,id;
bool operator < (const query &w) const {
return (belong[l]^belong[w.l])?(belong[l]<belong[w.l]):((belong[r]^belong[w.r])?((belong[l]&1)?belong[r]<belong[w.r]:belong[r]>belong[w.r]):t<w.t);
}
}Q[MAXN];
void Init_belong() {
int size=pow(n,2.0/3.0);
int bnum=ceil(n/size);
for(int i=1;i<=bnum;i++)
for(int j=size*(i-1)+1;j<=size*i&&j<MAXN;j++)
belong[j]=i;
}
struct modify {
int p,c;
}C[MAXN];
void Add(int x) {
sum+=(!cnt[a[x]]);
cnt[a[x]]++;
}
void Del(int x) {
cnt[a[x]]--;
sum-=(!cnt[a[x]]);
}
void Modify(int p,int c) {
if(l<=p&&p<=r)
Del(p);
if(l<=p&&p<=r)
Add(p);
}
void Do_change(int t) {
int p=C[t].p,&c=C[t].c;
if(l<=p&&p<=r)
Del(p);
swap(c,a[p]);
if(l<=p&&p<=r)
Add(p);
}
int main()
{
scanf("%d%d",&n,&m);
Init_belong();
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
qm=cm=0;
while(m--) {
char op[2];
scanf("%s",op);
if(op[0]=='Q') {
int l,r;
scanf("%d%d",&l,&r);
Q[++qm]={l,r,cm,qm};
}
else {
int p,c;
scanf("%d%d",&p,&c);
C[++cm]={p,c};
}
}
Add(1),l=r=1,tag=0;
sort(Q+1,Q+qm+1);
for(int i=1;i<=qm;i++) {
int ql=Q[i].l,qr=Q[i].r,qt=Q[i].t;
while(ql<l) Add(--l);
while(ql>l) Del(l++);
while(qr<r) Del(r--);
while(qr>r) Add(++r);
while(qt>tag) Do_change(++tag);
while(qt<tag) Do_change(tag--);
ans[Q[i].id]=sum;
}
for(int i=1;i<=qm;i++)
printf("%d\n",ans[i]);
}
luogu_sp10707
// 树上莫队
#include<bits/stdc++.h>
using namespace std;
const int MAXN=int(4e4+5);
const int MAXM=int(1e5+5);
#define pb push_back
int n,m;
vector<int> G[MAXN];
int dep[MAXN],p[MAXN][22],lg[MAXN];
int col[MAXN],cnt[MAXM];
bool vis[MAXN];
int st[MAXN],ed[MAXN],name[2*MAXN],idx;
int l,r,sum;
int belong[MAXN*2],ans[MAXM];
struct query {
int l,r,lca,id;
bool operator < (const query &W) const {
return (belong[l]^belong[W.l])?belong[l]<belong[W.l]:((belong[l]&1)?belong[r]<belong[W.r]:belong[r]>belong[W.r]);
}
}Q[MAXM];
#define Add(p) sum+=(!cnt[col[name[p]]]++)
#define Del(p) sum-=(!--cnt[col[name[p]]])
void Init_lg() {
for(int i=1;i<=n;i++)
lg[i]=lg[i-1]+(1<<lg[i-1]==i);
}
void Init_belong() {
int size=pow(2*n,2.0/3.0);
int bnum=ceil(2*n/size);
for(int i=1;i<=bnum;i++)
for(int j=(i-1)*bnum+1;j<=i*bnum&&j<=2*n;j++)
belong[j]=i;
}
void Change(int p) {
if(vis[p]) Del(p);
else Add(p);
vis[p]^=1;
}
void Dfs0(int u,int fa) {
st[u]=++idx;
name[idx]=u;
dep[u]=dep[fa]+1;
p[u][0]=fa;
for(int i=1;(1<<i)<=dep[u];i++)
p[u][i]=p[p[u][i-1]][i-1];
for(auto v:G[u])
if(v!=fa)
Dfs0(v,u);
ed[u]=++idx;
name[idx]=u;
}
int lca(int u,int v) {
if(dep[u]<dep[v])
swap(u,v);
while(dep[u]>dep[v])
u=p[u][lg[dep[u]-dep[v]]-1];
if(u==v)
return u;
for(int k=lg[dep[u]];k>=0;k--)
if(p[u][k]!=p[v][k])
u=p[u][k],v=p[v][k];
return p[u][0];
}
int main()
{
scanf("%d%d",&n,&m);
Init_lg();
Init_belong();
for(int i=1;i<=n;i++)
scanf("%d",&col[i]);
for(int i=1;i<=n-1;i++) {
int u,v;
scanf("%d%d",&u,&v);
G[u].pb(v),G[v].pb(u);
}
Dfs0(1,0);
for(int i=1;i<=m;i++) {
int u,v,ql,qr,id_lca=0;
scanf("%d%d",&u,&v);
if(st[u]>st[v]) swap(st[u],st[v]);
int fa=lca(u,v);
if(fa==u) ql=st[u],qr=st[v];
else ql=ed[u],qr=st[v],id_lca=st[fa]; // 注意这里lca传的编号
Q[i]={ql,qr,id_lca,i};
}
Add(1),l=r=1;
sort(Q+1,Q+m+1);
for(int i=1;i<=m;i++) {
int ql=Q[i].l,qr=Q[i].r;
while(ql>l) Change(l++);
while(ql<l) Change(--l);
while(qr>r) Change(++r);
while(qr<r) Change(r--);
if(Q[i].lca)
Change(Q[i].lca);
ans[Q[i].id]=sum;
if(Q[i].lca)
Change(Q[i].lca);
}
for(int i=1;i<=m;i++)
printf("%d\n",ans[i]);
}
莫队基础板子
#include<bits/stdc++.h>
using namespace std;
const int MAXN=int(1e6+5);
int n,m;
int a[MAXN],cnt[MAXN],ans[MAXN];
int l,r,sum;
int belong[MAXN];
struct query {
int l,r,id;
/*
bool operator < (const query &w) const {
return (l!=w.l)?l<w.l:r<w.r;
}
*/
bool operator < (const query &w) const {
return (l^w.l)?l<w.l:(l&1)?r<w.r:r>w.r;
}
}Q[MAXN];
void Init_belong() {
int size=sqrt(n);
int bnum=ceil((double)n/size);
for(int i=1;i<=bnum;i++)
for(int j=(i-1)*size+1;j<=i*size&&j<MAXN;j++)
belong[j]=i;
}
void Add(int i) {
sum+=(!cnt[a[i]]);
cnt[a[i]]++;
}
void Del(int i) {
cnt[a[i]]--;
sum-=(!cnt[a[i]]);
}
int main()
{
scanf("%d",&n);
Init_belong();
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
Add(1),l=1,r=1;
scanf("%d",&m);
for(int i=1;i<=m;i++) {
int ql,qr;
scanf("%d%d",&ql,&qr);
Q[i]={ql,qr,i};
}
sort(Q+1,Q+m+1);
for(int i=1;i<=m;i++) {
int ql=Q[i].l,qr=Q[i].r;
while(ql>l) Del(l++);
while(ql<l) Add(--l);
while(qr>r) Add(++r);
while(qr<r) Del(r--);
ans[Q[i].id]=sum;
}
for(int i=1;i<=m;i++)
printf("%d\n",ans[i]);
}