论 · 分治
cdq | [SDOI2011] 拦截导弹
某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度、并且能够拦截任意速度的导弹,但是以后每一发炮弹都不能高于前一发的高度,其拦截的导弹的飞行速度也不能大于前一发。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。
在不能拦截所有的导弹的情况下,我们当然要选择使国家损失最小、也就是拦截导弹的数量最多的方案。但是拦截导弹数量的最多的方案有可能有多个,如果有多个最优方案,那么我们会随机选取一个作为最终的拦截导弹行动蓝图。
我方间谍已经获取了所有敌军导弹的高度和速度,你的任务是计算出在执行上述决策时,每枚导弹被拦截掉的概率。
- 对于 100 % 100\% 100% 的数据, 1 ≤ n ≤ 5 × 1 0 4 1\le n\le 5\times 10^4 1≤n≤5×104, 1 ≤ h i , v i ≤ 1 0 9 1\le h_i,v_i\le 10^9 1≤hi,vi≤109。
cdq是谁啊?qp?
cdq,是一种通用的分治算法。cdq:陈丹琦
cdq用来解决3维或者更高维度的偏序问题,就是类似于求满足a_i<a_j,b_i<b_j,c_i<c_j的(i,j)数量。
那么为什么cdq可以完成多维度偏序的问题,那么多排序不会导致某一维度的顺序被之后的排序破坏吗?
以三维偏序为例。
先按 x x x排序。分治时每次将前半边、后半边分别按 y y y排序。虽然现在 x x x的顺序被打乱了,但是前半边还是都小于后半边的,所以要是只计算前半边对后半边的偏序关系,是不会受到 x x x的影响的。
维护后一半的指针 i i i,前一半的指针 j j j,每次将i后移一位时,若 y j ≤ y i y_j≤y_i yj≤yi则不断后移 j j j,并不断将 z j z_j zj加入树状数组。然后再查询树状数组中有多少数小于等于 z i z_i zi(树状数组求前缀和)。
最后要清空树状数组。
归并排序——cdq分治的基础
归并排序,递归合并
归并排序是什么?它是cdq分治的基础,是排序大厦的地基,是逆序对问题的终极良药。
来看模板
void merge(int left, int mid, int right) {int i, j, k;//三个指针,指向左部开头,右部开头,tmp开头(防止冲突,这里也写左部开头)i,j双指针遍历两部,那个小就往tmp中放那个,直到有一部被放完了;将剩下一部放进tmp;将tmp复制回原数组
}void merge_sort(int left, int right) {往左右两部递归;合并左右两部;
}
树状数组都不会?别学OI了!
inline int lowbit(int x) { return x & -x; }void add(int x, int v = 1) {//加,是从x一直加lowbit到顶while (x <= N) {tr[x] += v;x += lowbit(x);}
}int query(int x) {//查,是从x一直减lowbit到0int res = 0;while (x) {res += tr[x];x -= lowbit(x);}return res;
}!访问tr[]一定都是按x访问而不是lowbit(x)
给我模板!
cdq之树状数组写法
void cdq(l,r){往左右两部递归;归并合并左右两部;归并过程中维护树状数组;复原树状数组;
}
/int n, k;
int ans[N], d[N];
int tr[N];
struct node {int a, b, c, *id;
} a[N], b[N], c[N], tmp[N];int stk[N], top;inline int lowbit(int x) { return x & -x; }void add(int x, int v = 1) {while (x <= N) {// cerr << x << endl;tr[x] += v;x += lowbit(x);}
}int query(int x) {int res = 0;while (x) {res += tr[x];x -= lowbit(x);}return res;
}void cdq(int l, int r) {// cout << l << ' ' << r << endl;if (l == r)return;int mid = l + r >> 1;cdq(l, mid);cdq(mid + 1, r);int i = l, j = mid + 1, k = l - 1;while (i <= mid && j <= r) {if (a[i].b <= a[j].b) {// *a[i].id += query(a[i].c);add(a[i].c);stk[++top] = a[i].c;tmp[++k] = a[i];i++;} else {*a[j].id += query(a[j].c);// cerr<<a[j].id-ans<<':'<< query(a[j].c)<<endl;// add(a[j].c);// stk[++top] = a[j].c;tmp[++k] = a[j];j++;}}while (i <= mid)tmp[++k] = a[i++];while (j <= r) {tmp[++k] = a[j];// cerr<<j<<'-'<< query(a[j].c)<<endl;*a[j].id += query(a[j].c);j++;}while (top) {add(stk[top--], -1);}memset(tr,0,sizeof tr);// cerr << "dbg:" << l << ' ' << r << endl;for (int i = l; i <= r; i++) {// cerr << tmp[i].a << ' ' << tmp[i].b << ' ' << tmp[i].c << endl;a[i] = tmp[i];}
}
bool cmp(node x, node y) {if (x.a == y.a && x.b == y.b)return x.c < y.c;if (x.a == y.a)return x.b < y.b;return x.a < y.a;
}
signed main() {n = rd, k = rd;for (int i = 1; i <= n; i++) {a[i].a = rd, a[i].b = rd, a[i].c = rd, a[i].id = &ans[i];}sort(a + 1, a + n + 1, cmp);for (int i = n - 1; i; i--) {if (a[i].a == a[i + 1].a && a[i].b == a[i + 1].b && a[i].c == a[i + 1].c)*a[i].id = *a[i + 1].id + 1;}// cerr << "OK";cdq(1, n);for (int i = 1; i <= n; i++)d[ans[i]]++;for (int i = 0; i < n; i++)printf("%lld\n",d[i]);
}
例题代码
cdq分治优化dp,三维偏序,注意这里的递归顺序应是左中右
c++
/*
CB Ntsc111
*/#include <bits/stdc++.h>
using namespace std;#define ull unsigned int
#define pii pair<int, int>
#define pf to
#define ps second
#define int long long#define err cerr << "Error"
#define rd read()#define ot write
#define nl putchar('\n')
int read() {int xx = 0, ff = 1;char ch = getchar();while (ch < '0' || ch > '9') {if (ch == '-')ff = -1;ch = getchar();}while (ch >= '0' && ch <= '9')xx = xx * 10 + (ch - '0'), ch = getchar();return xx * ff;
}
void write(int out) {if (out < 0)putchar('-'), out = -out;if (out > 9)write(out / 10);putchar(out % 10 + '0');
}const int N=50005;
struct node{int x,y,z;int dp;double f;
}a[N];
int n;
int t2[N];
double t1[N];
int dp1[N],dp2[N];
double f1[N],f2[N];
bool cmp1(node i,node j){return i.x==j.x?(i.y==j.y?i.z<j.z:i.y>j.y):i.x>j.x;
}
bool cmp2(node i,node j){return i.x==j.x?(i.y==j.y?i.z<j.z:i.y<j.y):i.x<j.x;
}
bool cmp3(node i,node j){return i.y>j.y;
}
bool cmp4(node i,node j){return i.y<j.y;
}
int lowbit(int x){return x&-x;
}
void update(int x,int dp,double f){while(x<=n){if(t2[x]<dp){t2[x]=dp;t1[x]=f;}else if(t2[x]==dp)t1[x]+=f;x+=lowbit(x);}return ;
}
int query1(int x){int ans=0;while(x){ans=max(ans,t2[x]);x-=lowbit(x);}return ans;
}
double query2(int x,int dp){double ans=0;while(x){if(t2[x]==dp)ans+=t1[x];x-=lowbit(x);}return ans;
}
void del(int x){while(x<=n){t2[x]=t1[x]=0;x+=lowbit(x);}return ;
}
bool check(int p,int q,int opt){if(opt==1)return a[p].y>=a[q].y;return a[p].y<=a[q].y;
}
void merge_sort(int l,int r,int opt){int mid=l+((r-l)>>1);int p=l,q=mid+1;while(p<=mid&&q<=r){if(check(p,q,opt)){update(a[p].z,a[p].dp,a[p].f);p++;}else{int val=query1(a[q].z)+1;if(a[q].dp<val){a[q].dp=val;a[q].f=query2(a[q].z,val-1);}else if(a[q].dp==val)a[q].f+=query2(a[q].z,val-1);q++;}}while(q<=r){int val=query1(a[q].z)+1;if(a[q].dp<val){a[q].dp=val;a[q].f=query2(a[q].z,val-1);}else if(a[q].dp==val)a[q].f+=query2(a[q].z,val-1);q++;}for(int i=l;i<p;i++)del(a[i].z);return ;
}
void cdq(int l,int r,int opt){if(l==r)return ;int mid=l+((r-l)>>1);cdq(l,mid,opt);if(opt==1){sort(a+l,a+mid+1,cmp3);sort(a+mid+1,a+r+1,cmp3);}else{sort(a+l,a+mid+1,cmp4);sort(a+mid+1,a+r+1,cmp4);}merge_sort(l,r,opt);if(opt==1)sort(a+mid+1,a+r+1,cmp1);else sort(a+mid+1,a+r+1,cmp2);cdq(mid+1,r,opt);return ;
}
signed main(){n=rd;for(int i=1;i<=n;i++){a[i].x=rd;a[i].y=rd;a[i].z=i;a[i].dp=1;a[i].f=1;}sort(a+1,a+n+1,cmp1);cdq(1,n,1);int ans=0;for(int i=1;i<=n;i++){dp1[a[i].z]=a[i].dp;f1[a[i].z]=a[i].f;ans=max(ans,dp1[a[i].z]);}printf("%lld\n",ans);for(int i=1;i<=n;i++){a[i].z=n-a[i].z+1;a[i].dp=1;a[i].f=1;}sort(a+1,a+n+1,cmp2);cdq(1,n,2);double k=0;for(int i=1;i<=n;i++){dp2[n-a[i].z+1]=a[i].dp;f2[n-a[i].z+1]=a[i].f;if(dp2[n-a[i].z+1]==ans)k+=f2[n-a[i].z+1];}for(int i=1;i<=n;i++){if(dp1[i]+dp2[i]==ans+1)printf("%.5lf ",f1[i]*f2[i]/k);elseprintf("0.00000 ");}return 0;
}
论 · 图论
tarjan | [国家集训队] 稳定婚姻
我们已知 n n n 对夫妻的婚姻状况,称第 i i i 对夫妻的男方为 B i B_i Bi,女方为 G i G_i Gi。若某男 B i B_i Bi 与某女 G j G_j Gj 曾经交往过(无论是大学,高中,亦或是幼儿园阶段, i ≤ j i \le j i≤j),则当某方与其配偶(即 B i B_i Bi 与 G i G_i Gi 或 B j B_j Bj 与 G j G_j Gj)感情出现问题时,他们有私奔的可能性。不妨设 B i B_i Bi 和其配偶 G i G_i Gi 感情不和,于是 B i B_i Bi 和 G j G_j Gj 旧情复燃,进而 B j B_j Bj 因被戴绿帽而感到不爽,联系上了他的初恋情人 G k G_k Gk ……一串串的离婚事件像多米诺骨牌一般接踵而至。若在 B i B_i Bi 和 G i G_i Gi 离婚的前提下,这 2 n 2n 2n 个人最终依然能够结合成 n n n 对情侣,那么我们称婚姻 i i i 为不安全的,否则婚姻 i i i 就是安全的。
给定所需信息,你的任务是判断每对婚姻是否安全。
对于 100 % 100\% 100% 的数据,所有姓名字符串中只包含英文大小写字母,大小写敏感,长度不大于 8 8 8,保证每对关系只在输入文件中出现一次,输入文件的最后 m m m 行不会出现未在之前出现过的姓名,这 2 n 2n 2n 个人的姓名各不相同, 1 ≤ n ≤ 4000 1 \le n \le 4000 1≤n≤4000, 0 ≤ m ≤ 20000 0 \le m \le 20000 0≤m≤20000。
欧!我的塔杨!偶不!
Tarjan,图论万能算法,SCC,eDCC,vDCC,缩点等都离不开塔杨。Tarjan:算法和其创作者的名字
Tarjan是一种神奇的算法,可以使用每个点的dfn和low值划分上面的SCC等。
我们要先明确dfn和low的意义
-
dfn:我们访问点是通过树边来访问的,即我们的dfs不会走成环。所以dfn_i代表着点i第一次被访问的时间戳。
-
low:表示点i可以访问到的最小时间戳。
那么low怎么求呢?
我们发现,只要发现当前点u的邻居v之前已经被访问了,那么就可以用dfn_v来更新low_u。对于其他的邻居,若没有访问过还是正常去访问它。
在dfs从u回溯时,设回溯到u的父亲x(访问点的路径上的u的上一个点),此时因为在从x处dfs下去时u还没有被访问,因此不能使用dfn_u去更新dfn_x,而应该使用low_u来传递返回到u处的信息。
SCC,eDCC,vDCC……什么呀都是什么呀!
好吧,不要蒙,我们来一一解释。
-
SCC
强连通分量(有向图上)。像SCC这些其实都是一个集合的名称,即我们称某某点属于同一个SCC这种类型的话。
那么什么是强连通分量呢?我们发过来看性质——强连通分量中的任意两个点都可以互相到达,并且分量是极大的。
我们发现,若要满足任意点之间互相可达,出来单一的点是SCC外,只有一种情况了——这个子图是一个环
所以我们在Tarjan算法中就是找环。那么怎么找呢?好好利用dfn和low,我们发现,只要发现当前点u的邻居v之前已经被访问了,那么就可以用dfn_v来更新low_u。对于其他的邻居,若没有访问过还是正常去访问它。
在dfs从u回溯时,设回溯到u的父亲x(访问点的路径上的u的上一个点),此时因为在从x处dfs下去时u还没有被访问,因此不能使用dfn_u去更新dfn_x,而应该使用low_u来传递返回到u处的信息。
为了求出SCC,我们需要记录一个栈,存放当前访问了哪些点(这些点都是没有被划分SCC的)。当在u出往下dfs全都回溯到u时,就可以判断u及从u往下经过的那些点有没有环了。
如果low_u≠dfn_u,说明u在一个环上。但是为了不重复计算,我们不可以在此处处理这个环
如果low_u=dfn_u,那么恰好说明u是一个环的根(或者是一个孤点),此时栈中的点从u及往上(上面是栈顶)都是属于这个环中的。标记这些点属于同一个SCC并且弹出。
这里用反证法可能更好理解——假设我们在low_u≠dfn_u时的u点处理SCC,那么我给你一条链,阁下作何解?要么成环,要么是链。如果是链,每一个点都是SCC;如果是环,那么假设u的第一个访问的这个环上的点,最终环上所有点的low值都会变成dfn_u,所以我们找一个最具代表性的点,那么就是u(low=dfn)你了!
-
eDCC
-
vDCC
要背模板,呜呜……
void tarjan(int x){//从节点x进入 给该点的low和dfn赋值;枚举邻点并往没有访问的(dfn=0)点dfs下去:更新low根据low和dfn的关系判断;
}
SCC
void tarjan(int x){//从节点x进入 if(scc[x])return;//如果已经是某个强连通分量里的点,停止函数//入x时,时间戳追溯值更新,入栈dfn[x]=low[x]=++tot;stk[++top]=x;instk[x]=1; for(int i=0;i<e[x].size();i++){//枚举x的邻点y int y=e[x][i];if(!dfn[y]){//如果y还没有访问过 tarjan(y);//向下走 low[x]=min(low[x],low[y]);//返回时更新 }else if(dfn[y]&&instk[y]){//说明 y被访问过 ->要么y是祖先(在x出通过返祖边访问到了),要么是左子树的点(在x通过横插边访问到了) low[x]=min(low[x],dfn[y]); }}if(dfn[x]==low[x]){//说明x是这个强连通分量的根 int y;++cnt;int flag=0;//记录当前强连通分量点的个数do{flag++;y=stk[top--];instk[y]=0;scc[y]=cnt;++siz[cnt];} while(y!=x); if(flag>1)ans++;}
}
例题代码
本题重点是怎么建图。
原题读完后,很容易想到建双边。
但是如果这样的话,我们无法判断。
所以我们在建图时,要把夫妻和情侣都拆开建图。
那怎么拆才能形成环呢?
有三种方案:
-
男 -> 女
-
女 -> 男
-
男 -> 女 和 女 -> 男分开
手推后发现第3种建图后通过Tarjan判断SCC即可知道有无环,也就知道是否安全了。
/*
CB Ntsc111
*/#include <bits/stdc++.h>
using namespace std;#define ull unsigned int
#define pii pair<int, int>
#define pf to
#define ps second
#define int long long#define err cerr << "Error"
#define rd read()#define ot write
#define nl putchar('\n')
int read() {int xx = 0, ff = 1;char ch = getchar();while (ch < '0' || ch > '9') {if (ch == '-')ff = -1;ch = getchar();}while (ch >= '0' && ch <= '9')xx = xx * 10 + (ch - '0'), ch = getchar();return xx * ff;
}
void write(int out) {if (out < 0)putchar('-'), out = -out;if (out > 9)write(out / 10);putchar(out % 10 + '0');
}map<string, int> mp;
const int N = 200000;
int cnt;
int h[N], nxt[N], to[N];
int tot;
void add(int u, int v) {nxt[++cnt] = h[u];h[u] = cnt;to[cnt] = v;
}
int dfn[N], low[N], clo[N];
int vis[N], stk[N], top;
void tarjan(int u) {stk[++top] = u;dfn[u] = low[u] = ++tot;vis[u] = 1;for (int i = h[u], v = to[i]; i; i = nxt[i], v = to[i])if (!dfn[v]) {tarjan(v);low[u] = min(low[u], low[v]);} else if (vis[v])low[u] = min(low[u], dfn[v]);if (low[u] == dfn[u]) {clo[u] = u;vis[u] = 0;int y;while ((y = stk[top--]) != u)clo[y] = u, vis[y] = 0;}
}
signed main() {int n = rd, m;int u, v;string girl, boy;for (int i = 1; i <= n; ++i) {cin >> girl;u = mp[girl] = ++tot;cin >> boy;v = mp[boy] = ++tot;add(u, v);}m = rd;tot = 0;for (int i = 1; i <= m; ++i) {cin >> girl;cin >> boy;v = mp[girl];u = mp[boy];add(u, v);}for (int i = 1; i <= (n << 1); ++i)if (!dfn[i])tarjan(i);for (int i = 1; i <= n; ++i)if (clo[i << 1] == clo[(i << 1) - 1])puts("Unsafe");elseputs("Safe");return 0;
}
最短路 | [TJOI2012] 桥
有 n n n 个岛屿, m m m 座桥,每座桥连通两座岛屿,桥上会有一些敌人,玩家只有消灭了桥上的敌人才能通过,与此同时桥上的敌人会对玩家造成一定伤害。而且会有一个大 Boss 镇守一座桥,以玩家目前的能力,是不可能通过的。而 Boss 是邪恶的, Boss 会镇守某一座使得玩家受到最多的伤害才能从岛屿 1 1 1 到达岛屿 n n n(当然玩家会选择伤害最小的路径)。问,Boss 可能镇守的桥有哪些。
注意可以有重边和自环。
-
100 % 100\% 100% 的数据, 1 ≤ n ≤ 100000 , 1 ≤ m ≤ 200000 , 1 ≤ c ≤ 10000 1 ≤ n ≤ 100000, 1 ≤ m ≤ 200000, 1 ≤ c ≤ 10000 1≤n≤100000,1≤m≤200000,1≤c≤10000;
-
数据保证玩家可以从岛屿 1 1 1 到达岛屿 n n n。
最短路?看起来很好吃的样子
最短路的主流算法有dijkstra和SPFA,后者主要是用来判断负环存在的。
两个算法的区别就在于vis数组的含义上,以及维护的数据结构。
dijkstra是什么?
模板了。
用优先队列维护(优化)没有扩散的、dis最小的点,由此点来扩散。vis_i标记是否从点i扩散过。
void djstr(int rt) {pq.push(make_pair(0,rt));int u=rt; //先从起点开始查for(int i=1; i<=n; i++)dis[i]=2147483647; //初始化边权dis[rt]=0;
// vis[rt]=1;//别写错!!while(pq.size()) { //搜完全图u=pq.top().second;pq.pop();if(vis[u])continue;//记得continuevis[u]=1;for(int i=0;i<e[u].size();i++){int v=e[u][i].nxt,w=e[u][i].dis;if(!vis[v]&&dis[u]+w<dis[v]){dis[v]=dis[u]+w; //更新pq.push(make_pair(-dis[v],v));}}}
}
SPFA是什么?
模板了。
类似广搜,vis_i标记点i是否在广搜等待队列中。
void spfa(int rt) {dis[rt]=0;vis[rt]=1;q.push(rt);while(q.size()) { //类似广搜int u=q.front();q.pop();vis[u]=0;for(int i=h[u]; i; i=edge[i].next) {int nxt=edge[i].to;if(dis[nxt]<=dis[u]+edge[i].dis)continue;dis[nxt]=dis[u]+edge[i].dis;if(!vis[nxt]) {vis[nxt]=1;q.push(nxt);}}}}
例题代码
把题目抽象一下,就是求最短路路径上、在最短路……第k短路中出现次数最多的那条边。
首先我们枚举最短路路径上的边断掉,那么我们现在只需要计算在断掉最短路上的任意一条边后形成的新的最短路最大值是多少。
网络流 | [NOI2008] 志愿者招募
申奥成功后,布布经过不懈努力,终于成为奥组委下属公司人力资源部门的主管。布布刚上任就遇到了一个难题:为即将启动的奥运新项目招募一批短期志愿者。经过估算,这个项目需要 n n n 天才能完成,其中第 i i i 天至少需要 a i a_i ai 个人。布布通过了解得知,一共有 m m m 类志愿者可以招募。其中第 i i i 类可以从第 s i s_i si 天工作到第 t i t_i ti 天,招募费用是每人 c i c_i ci 元。新官上任三把火,为了出色地完成自己的工作,布布希望用尽量少的费用招募足够的志愿者,但这并不是他的特长!于是布布找到了你,希望你帮他设计一种最优的招募方案。
1 ≤ n ≤ 1000 1\leq n\leq 1000 1≤n≤1000, 1 ≤ m ≤ 10000 1\leq m\leq 10000 1≤m≤10000,题目中其他所涉及的数据均不超过 2 31 − 1 2^{31}-1 231−1。
网络流是什么?看起来好像不是OI的内容呀
网络流是在网络上跑的一系列算法。网络:有源点和汇点的有向图。
网络流最典型的问题就是最大流,而最短路最典型的问题就是城市管道了。
其实是一个很看重图论建模的算法,其他的就是板子了。
建模?怎么建模?
自己悟吧!
好吧,那……那板子总得看看吧?
最大流 dinic
void add(int x,int y,int b){链式前向星建图,奇偶建图(原边,反悔边)
}bool bfs(){//建分层图//找增广路,就是广搜这幅图且只走有空余流量(e[].c)的边//看最后能否从s到达t,可以就返回1//注意在增广路时图一直不变,不用cpy//不要清空d数组!!可以想象是一开始走的边很少,后面开始饱和了起来。广搜,初始化d[s]=1;枚举出边:若出边有空余流量且没有被分层,v入队,d[v]=d[u]+1.若已经到达t,直接返回1;返回0;
}int dfs(int u,int mf){//当前点u,(这条路径上)走到u时的剩余流量根据d[v]来判断当前边是不是在增广路中知道的那条新边,并且检查是否有空余流量当前弧优化 枚举边,可以走的话就走,并且调整路径上的流量限制回溯时,更新反悔边和原边流量记录从该点可以流出的流量更新路径到该点的流量的剩余流量,若该点的流量的剩余流量,则break;若该点没有流量空余流出,标记该点无用(d[u]=0)int dinic(){一直bfs,如果可以bfs通过空余流量一直到t,那么就说明还有可行流,继续dfs,并且把答案加上新增流量;
}
/*ACACACACACACAC///
Code By Ntsc
/*ACACACACACACAC///
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e5;struct edge{int v,c,nxt;
}e[N];
int n,m,h[N],ans,idx=1,d[N],s,t,cur[N];
void add(int x,int y,int b){e[++idx]={y,b,h[x]};h[x]=idx;
}
bool bfs(){//对每个点进行分层 ,为dfs找增光路做准备 memset(d,0,sizeof d);queue<int>q;q.push(s);d[s]=1;//源点是第一层 while(q.size()){int u=q.front();q.pop();for(int i=h[u];i;i=e[i].nxt){int v=e[i].v;if(!d[v]&&e[i].c){//如果没有访问过v并且这条边有剩余容量 d[v]=d[u]+1;//v点位于u的下一层 q.push(v) ;if(v==t)return 1;}}}return 0;
}
int dfs(int u,int mf){//当前点u,(这条路径上)走到u时的剩余流量 //入33 下37 回42 离50 if(u==t)return mf;//如果已经到达汇点,直接返回int sum=0;//计算u点可以流出的流量之和 (e.g.当u=2时,最后sum=3+3+4+2+3)for(int i=cur[u];i;i=e[i].nxt){cur[u]=i;//记录从哪一条边走出去了 ,后面有用 int v=e[i].v;if(d[v]==d[u]+1&&e[i].c){//如果v在u的下一层 并且有剩余容量 int f=dfs(v,min(mf,e[i].c));//正如EK中的'mf[v]=min(mf[u],e[i].c);' //回e[i].c-=f;e[i^1].c+=f;//更新残留网,对照EK sum+=f;mf-=f;//类似八皇后,请思考! if(!mf)break;//优化.当在u的前几条分支已经流光了u的可用流量时,就不用考虑剩下的分支了 }} if(!sum)d[u]=0;//残枝优化.目前这条路没有可行流了 return sum;
}
int dinic(){//累加答案 int ans=0;while(bfs()){//可以找到增光路 memcpy(cur,h,sizeof h);//请思考! ans+=dfs(s,1e9);//还是那句话'//源点的流量上限为无穷大,即源点能为后面提供无限大的流量' }return ans;
}
signed main(){cin>>n>>m>>s>>t;for(int i=1;i<=m;i++){int u,v,w;cin>>u>>v>>w;add(u,v,w);add(v,u,0);} cout<<dinic()<<endl;return 0;
}
/*
CB Ntsc111
*/#include <bits/stdc++.h>
using namespace std;#define ull unsigned int
#define pii pair<int, int>
#define pf to
#define ps second
#define int long long#define err cerr << "Error"
#define rd read()#define ot write
#define nl putchar('\n')
int read() {int xx = 0, ff = 1;char ch = getchar();while (ch < '0' || ch > '9') {if (ch == '-')ff = -1;ch = getchar();}while (ch >= '0' && ch <= '9')xx = xx * 10 + (ch - '0'), ch = getchar();return xx * ff;
}
void write(int out) {if (out < 0)putchar('-'), out = -out;if (out > 9)write(out / 10);putchar(out % 10 + '0');
}const int M = 800;
const int mxxlog = 10;
int MOD = 1e9 + 57;
const int N = 1 + 4e3 + 3;
const int INF = 1e9;
const double eps = 1e-10;bool bfs(){//找增广路,就是广搜这幅图且只走有空余流量(e[].c)的边//看最后能否从s到达t,可以就返回1//注意在增广路时图一直不变,不用cpy//不要清空d数组!!可以想象是一开始走的边很少,后面开始饱和了起来。memset(d,0,sizeof d);queue<int> q;q.push(s);d[s]=1;while(q.size()){int u=q.front();q.pop();for(int i=h[u];i;i=e[i].nxt){int v=e[i].v;if(!d[v]&&e[i].c){d[v]=d[u]+1;q.push(v);if(v==t)return 1;}}}return 0;
}int dfs(int u,int mf){//当前点u,(这条路径上)走到u时的剩余流量//根据d[v]来判断当前边是不是在增广路中知道的那条新边,并且检查是否有空余流量if(u==t)return mf;int sum=0;for(int i=cur[u];i;i=e[i].nxt){cur[u]=i;//当前弧优化int v=e[i].v;if(d[v]==d[u]+1&&e[i].c){//可以走的话就走,并且调整路径上的流量限制int f=dfs(v,min(mf,e[i].c));e[i].c-=f;//更新反悔边和原边流量e[e^1].c+=f;sum+=f;//记录从该点可以流出的流量mf-=f;//更新路径到该点的流量的剩余流量if(!mf)break;//优化}}if(!sum)d[u]=0;//优化return sum;
}int dinic(){//可以bfs通过空余流量一直到t,那么就说明还有可行流,继续dfsint ans=0;while(bfs()){memcpy(cur,h,sizeof h);//将h复制一份给curans+=dfs(s,1e9);}return ans;
}
费用流又是上面呀??
论 · 树形结构
虚树 | [HEOI2014] 大工程
国家有一个大工程,要给一个非常大的交通网络里建一些新的通道。
我们这个国家位置非常特殊,可以看成是一个单位边权的树,城市位于顶点上。
在 2 2 2 个国家 a , b a,b a,b 之间建一条新通道需要的代价为树上 a , b a,b a,b 的最短路径的长度。
现在国家有很多个计划,每个计划都是这样,我们选中了 k k k 个点,然后在它们两两之间 新建 ( k 2 ) \dbinom{k}{2} (2k) 条新通道。
现在对于每个计划,我们想知道:
-
这些新通道的代价和。
-
这些新通道中代价最小的是多少。
-
这些新通道中代价最大的是多少。
对于 100 % 100\% 100% 的数据, 1 ≤ n ≤ 1 0 6 , 1 ≤ q ≤ 5 × 1 0 4 , ∑ k ≤ 2 × n 1\le n\le 10^6,1\le q\le 5\times 10^4,\sum k\le 2\times n 1≤n≤106,1≤q≤5×104,∑k≤2×n 。
每个测试点的具体限制见下表:
测试点编号 | n n n | 特殊性质 |
---|---|---|
1 ∼ 2 1\sim 2 1∼2 | ≤ 1 0 4 \le 10^4 ≤104 | |
3 ∼ 5 3\sim 5 3∼5 | ≤ 1 0 5 \le 10^5 ≤105 | 树的形态是链 |
6 ∼ 7 6\sim 7 6∼7 | ≤ 1 0 5 \le 10^5 ≤105 | |
8 ∼ 10 8\sim 10 8∼10 | ≤ 1 0 6 \le 10^6 ≤106 |
虚树?虚拟的树喵?
虚树,即把树上的无用点压缩或者删除,得到最终的简图,以降低在dfs在无用点的访问,降低时间复杂度。
构建虚拟树,首先我们要提取一些关键点。那么题目中一样就可以看出的关键点是必须的,并且wield可以构成一颗树,我们还需要保留构建点的lca以及原树根。
那么怎么样构建呢?这里给出两个方法
-
省事的方法
首先把关键点按欧dfs序排序,然后相邻两个点两两求lca加入即可。记得去重,这种方法可以保证不漏。证明见oiwiki
简略证明:若x是y的祖先(x<y),那么直接连边,因为我们是按dfs序排序的,所以x-y路径上不会有其它关键点。若x不是y的祖先(x<y),那么找到其lca,因为我们是按dfs序排序的,所以lca-y路径上不会有其它关键点,否则x就不是x了。
-
正义的方法
用单调栈。我们使用单调栈维护一条链,即我们时刻保证单调栈内的点属于一条链。如图
当我们发现新加入的一个点b不是当前链上的,那么就先弹出单调栈后加入的一些点弹出并同时把这些点和其父亲(即单调栈的下面一项)连边,知道单调栈中的点和b都在同一条链上。
注意如果b与a的lca不在栈中,那么我们需要把其lca先加入栈中。
模板?谢谢!
void build(){sort(p+1,p+m+1,cmp);//关键点按dfn排序for(int i=1;i<m;i++){a[++len]=p[i],a[++len]=lca(p[i],p[i+1]);//往虚树中添加点}a[++len]=p[m];sort(a+1,a+len+1,cmp);//排序,为了去重和接下来的建边len=unique(a+1,a+len+1)-a-1;for(int i=1;i<len;i++){int anc=lca(a[i],a[i+1]);add(anc,a[i+1]);//建边,因为之前按照了dfn排序,所以这里求lca只是为了确定父子关系,而不是往虚树中加入新的点}
}
例题代码
我们发现总点数是1e6的而有效点数只是1e5的,所以可以用虚树去掉无用点。
注意,虚树只是一种优化方法,具体算法还是看具体题目。
就像本题还是基于树形dp
对于第一问:直接统计所有树边对答案的贡献即可。
对于第2,3问:记f[x]表示在x的子树内离x距离最远的关键点的距离,g[x]表示在x的子树内离x距离最近的关键点的距离。
/*
CB Ntsc111
*/#include <bits/stdc++.h>
using namespace std;#define ull unsigned int
#define pii pair<int, int>
#define pf to
#define ps second
#define int long long#define err cerr << "Error"
#define rd read()#define ot write
#define nl putchar('\n')
int read() {int xx = 0, ff = 1;char ch = getchar();while (ch < '0' || ch > '9') {if (ch == '-')ff = -1;ch = getchar();}while (ch >= '0' && ch <= '9')xx = xx * 10 + (ch - '0'), ch = getchar();return xx * ff;
}
void write(int out) {if (out < 0)putchar('-'), out = -out;if (out > 9)write(out / 10);putchar(out % 10 + '0');
}const int N = 1e6 + 5;
struct Graph {int to, nxt;
} e[N << 2];
int H[N], h[N], tot;
void init() {memset(H, -1, sizeof(H));memset(h, -1, sizeof(h));
}
void add(int *h, int u, int v) {e[tot] = (Graph){v, h[u]};h[u] = tot++;
}
int fa[N], dep[N], sz[N], top[N], son[N], dfn[N], tim;
void dfs1(int x) {dfn[x] = ++tim;sz[x] = 1, dep[x] = dep[fa[x]] + 1;for (int i = H[x]; ~i; i = e[i].nxt) {int v = e[i].to;if (v == fa[x])continue;fa[v] = x;dfs1(v);sz[x] += sz[v];if (sz[v] > sz[son[x]])son[x] = v;}
}
void dfs2(int x, int tp) {top[x] = tp;if (son[x])dfs2(son[x], tp);for (int i = H[x]; ~i; i = e[i].nxt) {int v = e[i].to;if (v == fa[x] || v == son[x])continue;dfs2(v, v);}
}
int LCA(int x, int y) {while (top[x] != top[y]) {if (dep[top[x]] < dep[top[y]])swap(x, y);x = fa[top[x]];}return dep[x] < dep[y] ? x : y;
}// using Tree::LCA; using Tree::dfn; using Tree::dep;
int n, M, K, a[N];
bool key[N];
int f[N], g[N], s[N];
bool cmp(int i, int j) { return dfn[i] < dfn[j]; }
void build() {int stk[N], top;sort(&a[1], &a[K + 1], cmp);stk[top = 1] = 1;h[1] = -1;tot = 0;for (int i = 1; i <= K; i++) {key[a[i]] = 1;if (a[i] == 1)continue;int lca = LCA(stk[top], a[i]);if (lca != stk[top]) {while (dfn[lca] < dfn[stk[top - 1]]) {int u = stk[top], v = stk[top - 1];add(h, u, v), add(h, v, u);--top;}if (dfn[lca] > dfn[stk[top - 1]]) {h[lca] = -1;int u = stk[top], v = lca;add(h, u, v), add(h, v, u);stk[top] = lca;} else {int u = lca, v = stk[top--];add(h, u, v), add(h, v, u);}}h[a[i]] = -1, stk[++top] = a[i];}for (int i = 1; i < top; i++) {int u = stk[i], v = stk[i + 1];add(h, u, v), add(h, v, u);}
}
int ans1, ans2, ans3;
void Dp(int x, int fa) {s[x] = key[x], f[x] = 0, g[x] = (key[x] ? 0 : 1e9);for (int i = h[x]; ~i; i = e[i].nxt) {int v = e[i].to;if (v == fa)continue;Dp(v, x);}for (int i = h[x]; ~i; i = e[i].nxt) {int v = e[i].to, w = dep[v] - dep[x];if (v == fa)continue;ans1 += 1ll * (K - s[v]) * s[v] * w;if (s[x] > 0) {ans2 = min(ans2, g[x] + w + g[v]);ans3 = max(ans3, f[x] + w + f[v]);}g[x] = min(g[x], g[v] + w);f[x] = max(f[x], f[v] + w);s[x] += s[v];}key[x] = 0;
}
signed main() {init();n = rd;for (int i = 1; i < n; i++) {int u = rd, v = rd;add(H, u, v), add(H, v, u);}dfs1(1);dfs2(1, 1);M = rd;while (M--) {ans1 = 0, ans2 = 1e9, ans3 = 0;K = rd;for (int i = 1; i <= K; i++)a[i] = rd;build();Dp(1, 0);printf("%lld %lld %lld\n", ans1, ans2, ans3);}return 0;
}
树套树 | [ZJOI2007] 报表统计
小 Q 的妈妈是一个出纳,经常需要做一些统计报表的工作。今天是妈妈的生日,小 Q 希望可以帮妈妈分担一些工作,作为她的生日礼物之一。
经过仔细观察,小 Q 发现统计一张报表实际上是维护一个非负整数数列,并且进行一些查询操作。
在最开始的时候,有一个长度为 n n n 的整数序列 a a a,并且有以下三种操作:
-
INSERT i k
:在原数列的第 i i i 个元素后面添加一个新元素 k k k;如果原数列的第 i i i 个元素已经添加了若干元素,则添加在这些元素的最后(见样例说明)。 -
MIN_GAP
:查询相邻两个元素的之间差值(绝对值)的最小值。 -
MIN_SORT_GAP
:查询所有元素中最接近的两个元素的差值(绝对值)。
于是小 Q 写了一个程序,使得程序可以自动完成这些操作,但是他发现对于一些大的报表他的程序运行得很慢,你能帮助他改进程序么?
对于全部的测试点,保证 2 ≤ n , m ≤ 5 × 1 0 5 2 \le n, m \le 5\times10^5 2≤n,m≤5×105, 1 ≤ i ≤ n 1 \leq i \leq n 1≤i≤n, 0 ≤ a i , k ≤ 5 × 1 0 8 0 \leq a_i, k \leq 5 \times 10^8 0≤ai,k≤5×108。
什么树套什么树?
狭义树套树,就是指平衡树套线段树。
这里我们可以联系主席树,或者说是可持久化线段树。我们开一个数组记录每一颗线段树的根,并且使用平衡树来维护这个数组。
广义的树套树还有树状数组套主席树(非官方),其实就是带修区间k大值;以及其它。
少年,先写平衡树吧!
我们来写FHQ Treap
分析模板
重点理解传地址(即&x,&y)及其修改
//分裂 根据v将树划分为2个子树
void split(int i,int v,int &x,int &y){//i当前节点,v划分数值, 返回时x会指向左treap的根,y指向右treap的根到达树叶赋值全0退出;判断下去节点val://如果这个点的val<=v,那么它的左子树一定都<=v,但是右子树的root虽然>val,但我们不知道它的儿子们是否都>v,所以需要递归右子树//递归实现,tr[i].l保留原始值当前val<=v,说明当前节点是左节点,给x,递归确定y与tr[i].r归属;当前val>v,说明当前节点是右节点,给y,递归确定x与tr[i].l归属;pushup;//总结:i给x或y只与t[].val和v有关;哪边不确定递归搜哪边;脑海构建图示
}//合并 分裂的逆过程.递归缝合2个分裂的treap
int merge(int x,int y){// x,y分别是左右treap的根,这里要*前提条件*满足x,y子树值性质,即x是左部,y是右部,否则还需要保证val性质有节点为空,返回另外一个;判断两个指针的key:如果x的key<=y的key,那么就把y和x的右儿子合并(递归下去)为x的右儿子(上面要求y是右边);pushup(x);如果x的key>y的key,那么就把x和y的左儿子合并(递归下去)为y的左儿子(上面要求x是左边);pushup(y);//总结:那个key大那个作根,x左y右递归合并,别忘pushup根;
}
!要明确函数返回的最终结果.如merge返回的最终结果是一颗树的根,那么就需要和另外一个原有的节点共同组成左右儿子
预备操作
struct node{int l,r,val,key,size;
}tr[N];
int n,root,idx; //root记录根的编号 ,idx是对新的节点进行编号的变量int addnode(int v){tr[++idx].val=v;tr[idx].key=rand();tr[idx].size=1;return idx;//返回这个点在数组里的序号
}
void pushup(int i){//向上更新子树大小tr[i].size=tr[tr[i].l].size+tr[tr[i].r].size+1;
}
开展操作
void insert(int v){在v处劈开,新增一个点,然后合并两颗树和那个点;//相当于z是一个1个节点的树,把它先和x合并(因为x的val均<=v,保证了它的大小顺序,至于它会被放在x的根或者其他地方,凭借key来确定),再和y合并
}
void del(int v){分裂成3棵树,分别代表着<v,=v,>v,然后合并第1,3棵树;
}
int getk(int i,int k){//获取中序排序第k个值的编号递归,依据左右子树的大小来判断走左子树还是有子树,并且对k进行修改哦
}
void getpre(int v){//找到v的前驱 (即<v的最大的那个点)在v-1处劈开,变成<v(x)和>=v(y) 2个树,在子树x里面找到最后一个就是 <v(x)的最大的那个点合并x,y,复原根}
void getsuc(int v){//找到v的后驱 (即>v的最小的那个点)在v处劈开,变成<=v(x)和>v(y) 2个树,在子树y里面找到第一个(getk(y,1))就是 >v(x)的最小的那个点合并x,y,复原根
}
void getrank(int v){//查询val=v的点的排名(从小到大) 如果有重复的val=v的节点只计第一个,排序不去重在v-1处劈开,变成<v(x)和>=v(y) 2个树,子树x的大小就是val=v的点前面有几个点合并x,y,复原根
}!注意在合并时要更新根
/*
CB Ntsc111
*/#include <bits/stdc++.h>
using namespace std;#define ull unsigned int
#define pii pair<int, int>
#define pf to
#define ps second
// #define int long long#define err cerr << "Error"
#define rd read()#define ot write
#define nl putchar('\n')
int read() {int xx = 0, ff = 1;char ch = getchar();while (ch < '0' || ch > '9') {if (ch == '-')ff = -1;ch = getchar();}while (ch >= '0' && ch <= '9')xx = xx * 10 + (ch - '0'), ch = getchar();return xx * ff;
}
void write(int out) {if (out < 0)putchar('-'), out = -out;if (out > 9)write(out / 10);putchar(out % 10 + '0');
}const int N=1e5+5;struct node{int l,r,val,key,size;
}tr[N];
int n,root,idx; //root记录根的编号 ,idx是对新的节点进行编号的变量 int addnode(int v){tr[++idx].val=v;tr[idx].key=rand();tr[idx].size=1;return idx;//返回这个点在数组里的序号
}
void pushup(int i){//向上更新子树大小 tr[i].size=tr[tr[i].l].size+tr[tr[i].r].size+1;
}
//分裂 根据v将树划分为2个子树
void split(int i,int v,int &x,int &y){//i当前节点,v划分数值, 返回时x会指向左treap的根,y指向右treap】的根if(!i){//到达树叶 x=y=0;return ;}if(tr[i].val<=v){//如果这个点的val<=v,那么它的左子树一定都<=v,但是右子树的root虽然>v,但我们不知道它的儿子们是否都>v,所以需要递归右子树 x=i;split(tr[i].r,v,tr[i].r,y);//递归实现 }else{y=i;split(tr[i].l,v,x,tr[i].l);}pushup(i);
}
//合并 分裂的逆过程.递归缝合2个分裂的treap
int merge(int x,int y){// x,y分别是左右treap的根 if(!x||!y){return x+y;} if(tr[x].key<=tr[y].key){//如果x的key<=y的key,那么y就作为x的子树,且是右子树,递归合并x原来的右子树和ytr[x].r=merge(tr[x].r,y);pushup(x);return x;}else{tr[y].l=merge(x,tr[y].l);pushup(y);return y;}
}
void insert(int v){int x,y,z;split(root,v,x,y);z=addnode(v);root=merge(merge(x,z),y);//相当于z是一个1个节点的树,把它先和x合并(因为x的val均<=v,保证了它的大小顺序,至于它会被放在x的根或者其他地方,凭借key来确定),再和y合并
}
void del(int v){int x,y,z;//将来会分别指向3棵树,他们的节点val分别是<v,=v,>v split(root,v,x,z);//此时分成了2棵树,x指向的树是<=v的,y则是>v的 split(x,v-1,x,y);//再把x分成2棵树,把<v(x)的和=v(y)的拎出来 y=merge(tr[y].l,tr[y].r);//把y变成y的左右子树合并,相当于把根抛弃了 root=merge(merge(x,y),z);//重新合并
}
int getk(int i,int k){//获取中序排序第k个值的编号 if(k<=tr[tr[i].l].size)return getk(tr[i].l,k);if(k==tr[tr[i].l].size+1)return i;return getk(tr[i].r,k-tr[tr[i].l].size-1);
}
void getpre(int v){//找到v的前驱 (即<v的最大的那个点)int x,y;//x,y只是暂时存放一下劈开的2个子树的根,后面还要合并 split(root,v-1,x,y);//劈开,变成<v(x)和>=v(y) 2个树 int p=getk(x,tr[x].size);//在子树x里面找到最后一个就是 <v(x)的最大的那个点cout<<tr[p].val<<endl;root=merge(x,y);//别忘了合并
}
void getsuc(int v){//找到v的后驱 (即>v的最小的那个点)int x,y;split(root,v,x,y);//劈开,变成<=v(x)和>v(y) 2个树 int p=getk(y,1);//在子树y里面找到第一个就是 >v(x)的最小的那个点cout<<tr[p].val<<endl;//cout<<"OK";root=merge(x,y);//别忘了合并
}
void getrank(int v){//查询val=v的点的排名(从小到大) 如果有重复的val=v的节点只计第一个,排序不去重 int x,y;split(root,v-1,x,y);//劈开,变成<v(x)和>=v(y) 2个树 cout<<tr[x].size+1<<endl;//子树x的大小就是val=v的点前面有几个点 root=merge(x,y);
}
void getval(int k){//查询排名为k的节点的值int p=getk(root,k);cout<<tr[p].val<<endl;}signed main(){ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);cin>>n;for(int i=1;i<=n;i++){int opt,x;cin>>opt>>x;if(opt==1){insert(x);}if(opt==2){del(x);}if(opt==3){getrank(x);}if(opt==4){getval(x);}if(opt==5){getpre(x);}if(opt==6){getsuc(x);}}return 0;
}
例题代码
-
操作1:
vector
暴力加入 -
操作2:平衡树维护差值
-
操作3:平衡树维护数值
/*
CB Ntsc111
*/#include <bits/stdc++.h>
using namespace std;#define ull unsigned int
#define pii pair<int, int>
#define pf to
#define ps second
#define pb push_back
#define int long long#define err cerr << "Error"
#define rd read()#define ot write
#define nl putchar('\n')
int read() {int xx = 0, ff = 1;char ch = getchar();while (ch < '0' || ch > '9') {if (ch == '-')ff = -1;ch = getchar();}while (ch >= '0' && ch <= '9')xx = xx * 10 + (ch - '0'), ch = getchar();return xx * ff;
}
void write(int out) {if (out < 0)putchar('-'), out = -out;if (out > 9)write(out / 10);putchar(out % 10 + '0');
}const int INF = 1e9;
const int N = 2000005;int n, m, tot, M1 = INF, M2 = INF, a[N];
int b[N], root;
struct tree {int l, r, num;
} sgt[N];struct node {int v, ch[2], fa;
} tr[N];namespace Splay {
void rotate(int x) {int f = tr[x].fa, gf = tr[f].fa;int k = x == tr[f].ch[1];tr[x].fa = gf;if (gf)tr[gf].ch[f == tr[gf].ch[1]] = x;tr[f].ch[k] = tr[x].ch[k ^ 1], tr[tr[x].ch[k ^ 1]].fa = f;tr[f].fa = x, tr[x].ch[k ^ 1] = f;
}void splay(int x, int goal) {while (tr[x].fa != goal) {int f = tr[x].fa, gf = tr[f].fa;if (gf != goal)(tr[f].ch[1] == x) ^ (tr[gf].ch[1] == f) ? rotate(x) : rotate(f);rotate(x);}if (goal == 0)root = x;
}void insert(int x) {int u = root, ff = 0;while (u && tr[u].v != x) {ff = u;u = tr[u].ch[tr[u].v < x];}if (!u) {u = ++tot;tr[u].fa = ff;if (ff)tr[ff].ch[tr[ff].v < x] = u;tr[u].v = x;splay(u, 0);}
}void Find(int x) {int u = root;if (!u)return;while (tr[u].v != x && tr[u].ch[tr[u].v < x])u = tr[u].ch[tr[u].v < x];splay(u, 0);
}int next(int x, int k) {Find(x);int u = root;if (tr[u].v == x)return tr[u].v;if ((tr[u].v < x && !k) || (tr[u].v > x && k))return tr[u].v;u = tr[u].ch[k];while (tr[u].ch[k ^ 1])u = tr[u].ch[k ^ 1];return tr[u].v;
}
} // namespace Splaynamespace SGT {
void build(int p, int l, int r) {sgt[p].l = l, sgt[p].r = r;if (l == r) {sgt[p].num = abs(a[l] - a[l - 1]);return;}int mid = (l + r) >> 1;build(p << 1, l, mid), build(p << 1 | 1, mid + 1, r);sgt[p].num = min(sgt[p << 1].num, sgt[p << 1 | 1].num);
}void update(int p, int x, int num) {int mid = (sgt[p].l + sgt[p].r) >> 1;if (sgt[p].l == sgt[p].r) {sgt[p].num = num;return;}if (x <= mid)update(p << 1, x, num);elseupdate(p << 1 | 1, x, num);sgt[p].num = min(sgt[p << 1].num, sgt[p << 1 | 1].num);
}
} // namespace SGTusing namespace Splay;
using namespace SGT;signed main() {n = rd, m = rd;insert(INF), insert(-INF);a[0] = a[n + 1] = INF;for (int i = 1; i <= n; i++) {a[i] = rd;if (i != 1) {int l = next(a[i], 0), r = next(a[i], 1);M2 = min(M2, min(abs(l - a[i]), abs(r - a[i])));}insert(a[i]);b[i] = a[i];}build(1, 1, n);for (int i = 1; i <= m; i++) {string s;cin >> s;if (s[0] == 'I') {int x = rd, y = rd;int l = next(y, 0), r = next(y, 1);M2 = min(M2, min(abs(l - y), abs(r - y)));insert(y);M1 = min(M1, abs(b[x] - y));update(1, x + 1, abs(a[x + 1] - y));b[x] = y;} else if (s[4] == 'G')printf("%lld\n", min(M1, sgt[1].num));elseprintf("%lld\n", M2);}return 0;
}
点分治 | [国家集训队] 聪聪可可
聪聪和可可是兄弟俩,他们俩经常为了一些琐事打起来,例如家中只剩下最后一根冰棍而两人都想吃、两个人都想玩儿电脑(可是他们家只有一台电脑)……遇到这种问题,一般情况下石头剪刀布就好了,可是他们已经玩儿腻了这种低智商的游戏。
他们的爸爸快被他们的争吵烦死了,所以他发明了一个新游戏:由爸爸在纸上画 n n n 个“点”,并用 n − 1 n-1 n−1 条“边”把这 n n n 个“点”恰好连通(其实这就是一棵树)。并且每条“边”上都有一个数。接下来由聪聪和可可分别随机选一个点(当然他们选点时是看不到这棵树的),如果两个点之间所有边上数的和加起来恰好是 3 3 3 的倍数,则判聪聪赢,否则可可赢。
聪聪非常爱思考问题,在每次游戏后都会仔细研究这棵树,希望知道对于这张图自己的获胜概率是多少。现请你帮忙求出这个值以验证聪聪的答案是否正确。
对于 100 % 100\% 100% 的数据, n ≤ 2 × 1 0 4 n\leq 2 \times 10^4 n≤2×104。
点?哪里的点?
点分治,即在树上的节点处分治。
我们从应用出发,看看点分治主要的题型是什么:求树上长度<k的路径的条数。
那么我们应该怎么办呢?分治分治,那么我们就考虑每一个点,我们把这个点的儿子两两配对,然后就类比成分治的左右两部即可。
真实情况会复杂一些,我们看看模板就知道了。
点分治的四步操作:
1 .找出树的重心做根, getroot()(优化时间复杂度)
2 .求出子树中的各点到根的距离,getdis()
3 .对当前树统计答案, calc()
4 ,分治各个子树,重复以上操作,divide()
求树的重心先啦
void getrt(int x,int fa){//得到树的重心作为根,降低复杂度根据重心的第一求重心:即儿子的左子树的最大值最小的那个点就是重心;
}
分治的具体函数是?
void getrt(int x,int fa){//得到树的重心作为根,降低复杂度dfs求解;
}void getdis(int x,int fa){//获取x的某棵子树中的点到x的距离(x指的是calc在的x)dfs求解;
}void calc(int x){遍历儿子:求出这个儿子的子树中的点到x的距离,记录在dis数组中(和顺序无关)那dis数组和匹配池G进行匹配(类似BSGS)将dis数据加入匹配池G,清空dis;清空匹配池G;
}void divide(int x){计算当前子树答案;标记已经被计算;枚举儿子:注意信息为子树信息(xms,sum,rt)求出子树内的重心作为根往子树分治;
}
例题代码
/*
CB Ntsc111
*/#include <bits/stdc++.h>
using namespace std;#define ull unsigned int
#define pii pair<int, int>
#define pf to
#define ps second
#define int long long#define err cerr << "Error"
#define rd read()#define ot write
#define nl putchar('\n')
int read() {int xx = 0, ff = 1;char ch = getchar();while (ch < '0' || ch > '9') {if (ch == '-')ff = -1;ch = getchar();}while (ch >= '0' && ch <= '9')xx = xx * 10 + (ch - '0'), ch = getchar();return xx * ff;
}
void write(int out) {if (out < 0)putchar('-'), out = -out;if (out > 9)write(out / 10);putchar(out % 10 + '0');
}const int INF = 2e9;
const int N = 20005;int n, cnt = 0, root, sum, ans, t[5], h[N], f[N], son[N] = {}, d[N] = {};
bool vis[N] = {};
struct Edge {int to, next, w;
} e[N << 1];
int gi() {int x = 0;int t = 1;char ch = getchar();while ((ch < '0' || ch > '9') && ch != '-')ch = getchar();if (ch == '-')t = -1, ch = getchar();while (ch >= '0' && ch <= '9')x = x * 10 + ch - 48, ch = getchar();return x * t;
}
void add(int u, int v, int w) {e[++cnt] = (Edge){v, h[u], w};h[u] = cnt;
}
void getdeep(int u, int fa) {t[d[u]]++;for (int i = h[u]; i; i = e[i].next) {int v = e[i].to;if (v == fa || vis[v])continue;d[v] = (d[u] + e[i].w) % 3;getdeep(v, u);}
}
int calc(int u, int v) {t[0] = t[1] = t[2] = 0;d[u] = v;getdeep(u, 0);return t[1] * t[2] * 2 + t[0] * t[0];
}
void getroot(int u, int fa) {son[u] = 1;f[u] = 0;for (int i = h[u]; i; i = e[i].next) {int v = e[i].to;if (v != fa && !vis[v]) {getroot(v, u);son[u] += son[v];f[u] = max(f[u], son[v]);}}f[u] = max(f[u], sum - son[u]);if (f[u] < f[root])root = u;
}
void solve(int u) {ans += calc(u, 0);vis[u] = 1;for (int i = h[u]; i; i = e[i].next) {int v = e[i].to;if (vis[v])continue;ans -= calc(v, e[i].w);root = 0;sum = son[v];getroot(v, 0);solve(root);}
}
signed main() {n = gi();for (int i = 1; i < n; i++) {int u = gi(), v = gi(), w = gi() % 3;add(u, v, w), add(v, u, w);}sum = f[0] = n;root = 0;getroot(1, 0);solve(root);int t = __gcd(ans, n * n);printf("%lld/%lld\n", ans / t, n * n / t);return 0;
}
树链剖分 | [HEOI2016/TJOI2016] 树
在 2016 年,佳媛姐姐刚刚学习了树,非常开心。现在他想解决这样一个问题:给定一颗有根树,根为 1 1 1 ,有以下两种操作:
-
标记操作:对某个结点打上标记。(在最开始,只有结点 1 1 1 有标记,其他结点均无标记,而且对于某个结点,可以打多次标记。)
-
询问操作:询问某个结点最近的一个打了标记的祖先。(这个结点本身也算自己的祖先)
你能帮帮她吗?
100 % 100\% 100% 的数据, 1 ⩽ N , Q ⩽ 100000 1 \leqslant N, Q \leqslant 100000 1⩽N,Q⩽100000 。
剖分?难不成是……把树剖开??
树链剖分,就是把树分成若干个链条,不重不漏。
树链剖分有几个剖分策略,最常用的是重链剖分。
在树链剖分中,我们通常会把一个节点的儿子分成一个特殊儿子和若干个普通儿子,有特殊儿子继续当前的链条,而普通儿子则新开链条。
那么在重链剖分中,我们就是把子树大小最大的那个儿子作为特殊儿子,叫做“重儿子”,其他的叫做轻儿子。在每一条链的连头,我们记录这条链的信息,并且在链上的每一个点记录其链头的位置,方便快速访问这条链的信息。
构建和应用
构建需要两次dfs——第一次处理子树大小,第二次剖分、传递信息。
应用,我们就那子树修改和路径修改来说吧。
-
子树修改:子树是dfs序上连续的一个区间,我们把每个节点按dfs序映射到线段树上即可。
-
路径修改:为了快速解决这个问题,想必我们也需要把它映射要线段树上。那么怎么办呢?我们卡门面已经对树进行了剖分,那么我们不妨规定一下dfs序:优先遍历重儿子。那么此时我们的每一条链按dfs序映射到线段树上就也是一个连续的区间了。
我们把路径划分为若干条子链(子链可能是链的一部分,但是也是连续的区间),对于每一条子链,我们知道其链头的dfs序l,进入此链的位置的点的dfs序r,然后修改[l,r]即可。
很典的好叭,线段树和树剖板子
void add(int a,int b){e[a].push_back(b);e[b].push_back(a);//记得双向边 du[b]++;
}void dfs1(int u,int faa){//初始值1,0 处理深度与子树大小;
} void dfs2(int u,int t){树剖,记录链头下标;处理dfs序和当前点到链头的区间(即记录当前点u在当前链上映射到dfs序上的下标,即tail,head不用处理因为在链头已经记录了)return ;
}//SGT
struct tree{int l,r;//左右节点代表的区间int add,sum;//标记与区间和
};区间修改线段树;//spou
int query_road(int u,int v){让u,v中深的往上跳,直到u,v在同一条链上;查询u-v信息;
}void updt_road(int u,int v,int k){让u,v中深的往上跳,直到u,v在同一条链上;修改u-v信息;
}signed main(){输入;建树;预处理深度;树剖;建立线段树;回答询问;return 0;
}
例题代码
那么本题的树剖用在哪里呢?用来快速往上跳(这种题目通常也可以使用倍增来写)
-
做法1:假设询问点u的标记祖先,那么先看这条链上有没有标记。有——在[top(链头),u]的线段树上二分。没有——往上跳。
线段树维护的是区间和,有标记的为1,没有标记的为0,二分时往有标记的且区间满足的位置二分。可能需要多个分叉。
-
做法2:维护区间最值。因为一个子链对应一个区间,所以查询这个区间的最小值即可。
/*
CB Ntsc111
*/#include <bits/stdc++.h>
using namespace std;#define ull unsigned int
#define pii pair<int, int>
#define pf to
#define ps second
#define int long long#define err cerr << "Error"
#define rd read()#define ot write
#define nl putchar('\n')
int read() {int xx = 0, ff = 1;char ch = getchar();while (ch < '0' || ch > '9') {if (ch == '-')ff = -1;ch = getchar();}while (ch >= '0' && ch <= '9')xx = xx * 10 + (ch - '0'), ch = getchar();return xx * ff;
}
void write(int out) {if (out < 0)putchar('-'), out = -out;if (out > 9)write(out / 10);putchar(out % 10 + '0');
}const int N = 100000 + 10;struct edge{int next,to;
}r[N<<1];
int n,Q;
char op;
int head[N],tot;inline void add(int u,int v)
{r[++tot]=(edge){head[u],v};head[u]=tot;r[++tot]=(edge){head[v],u};head[v]=tot;
}int dep[N],sz[N],fa[N],son[N],top[N],id[N],cnt;void dfs1(int u,int father)
{fa[u]=father,dep[u]=dep[father]+1,sz[u]=1;for(int e=head[u];e;e=r[e].next){int v=r[e].to;if(v==father)continue;dfs1(v,u);sz[u]+=sz[v];if(sz[v]>sz[son[u]])son[u]=v;}
}void dfs2(int u,int topf)
{top[u]=topf;id[u]=++cnt;if(!son[u])return ;dfs2(son[u],topf);for(int e=head[u];e;e=r[e].next){int v=r[e].to;if(v==fa[u]||v==son[u])continue;dfs2(v,v);}
}int tr[N<<2],tag[N<<2];
#define lson pos<<1
#define rson pos<<1|1
void pushdown(int pos)
{if(!tag[pos])return ;int k=tag[pos];tag[pos]=0;tr[lson]=dep[k]>dep[tr[lson]]?k:tr[lson];tr[rson]=dep[k]>dep[tr[rson]]?k:tr[rson];tag[lson]=dep[k]>dep[tag[lson]]?k:tr[lson];tag[rson]=dep[k]>dep[tag[rson]]?k:tr[rson];
}void change(int pos,int l,int r,int L,int R,int x)
{if(l>R||r<L)return ;if(l>=L&&r<=R){tr[pos]=dep[x]>dep[tr[pos]]?x:tr[pos];tag[pos]=dep[x]>dep[tag[pos]]?x:tag[pos];return ;}pushdown(pos);int mid=(l+r)>>1;if(dep[x]>dep[tr[lson]])change(lson,l,mid,L,R,x);if(dep[x]>dep[tr[rson]])change(rson,mid+1,r,L,R,x);tr[pos]=dep[tr[lson]]<dep[tr[rson]]?tr[lson]:tr[rson];
}int query(int pos,int l,int r,int x)
{if(l==x&&r==x)return tr[pos];pushdown(pos);int mid=(l+r)>>1;if(x<=mid)return query(lson,l,mid,x);else return query(rson,mid+1,r,x);
}signed main()
{n=rd,Q=rd;for(int i=1,u,v;i<n;i++)u=rd,v=rd,add(u,v);dfs1(1,0),dfs2(1,1);change(1,1,n,id[1],id[1]+sz[1]-1,1);int num;while(Q--){cin>>op;num=rd;if(op=='Q')printf("%lld\n",query(1,1,n,id[num]));if(op=='C')change(1,1,n,id[num],id[num]+sz[num]-1,num);}return 0;
}
主席树 | [SDOI2013] 森林
小 Z 有一片森林,含有 N N N 个节点,每个节点上都有一个非负整数作为权值。初始的时候,森林中有 M M M 条边。
小Z希望执行 T T T 个操作,操作有两类:
-
Q x y k
查询点 x x x 到点 y y y 路径上所有的权值中,第 k k k 小的权值是多少。此操作保证点 x x x 和点 y y y 连通,同时这两个节点的路径上至少有 k k k 个点。 -
L x y
在点 x x x 和点 y y y 之间连接一条边。保证完成此操作后,仍然是一片森林。
为了体现程序的在线性,我们把输入数据进行了加密。设 l a s t a n s lastans lastans 为程序上一次输出的结果,初始的时候 l a s t a n s lastans lastans 为 0 0 0。
对于一个输入的操作 Q x y k
,其真实操作为 Q x^lastans y^lastans k^lastans
。
对于一个输入的操作 L x y
,其真实操作为 L x^lastans y^lastans
。其中 ^
运算符表示异或,等价于 Pascal 中的 xor
运算符。
请写一个程序来帮助小 Z 完成这些操作。
对于 100 % 100\% 100% 的测试数据,所有节点的编号在 1 ∼ N 1\sim N 1∼N 的范围内。节点上的权值 ≤ 1 0 9 \le 10^9 ≤109。 M < N M<N M<N。
主席?哪个主席?是什么树?
主席树,可持久化线段树,或者说是保留历史版本的线段树。主席:hjt
我们回想一下线段树,还有它的单点修改过程。我们发现单点修改线段树就是修改了一条链上的节点。那么如果我们要保留历史版本,就不可以直接覆盖了——而是把这条链复制一份,然后在copy上修改。
如图:
那么我们怎么样访问各个版本的线段树呢?我们在新建版本时记录这个版本的线段树的根节点编号,这样以后就可以通过这个编号来找到这个版本的线段树了。
对于动态开点的信息继承:
我们发现在copy的点上,其两个儿子中最有一个点需要新建,另一个用旧版本的点即可。所以我们开点时两个指针,就像复制钥匙一样一个指向旧版本,一个用来开新版本并且顺便修改新信息。
模板来啦!
struct node{int lc,rc,v;
};void build(int &x,int l,int r){正常建树,不过是动态开点;
}void insert(int pre,int &now,int l,int r,int v){动态开点.新插入一个点,复制旧点的信息更新新点的信息(如它的两个子节点一个是原有的,一个是新开的);
}int query(int pre,int now,int l,int r,int k){//应用:查询区间l~r中第k大的数同时用两个指针指向l-1和r两个版本的权值线段树,然后得到区间[l,r]内在某个值域范围内的数字个数来判定走那个子节点;
}signed main() {输入,对于权值线段树则需要离散化;对询问一一回答;对创建的新版本的线段树需要记录其根节点编号;
}
例题代码
保证是std……
/*
CB Ntsc111
*/#include <bits/stdc++.h>
using namespace std;#define ull unsigned int
#define pii pair<int, int>
#define pf to
#define ps second
// #define int long long#define err cerr << "Error"
#define rd read()#define ot write
#define nl putchar('\n')
int read() {int xx = 0, ff = 1;char ch = getchar();while (ch < '0' || ch > '9') {if (ch == '-')ff = -1;ch = getchar();}while (ch >= '0' && ch <= '9')xx = xx * 10 + (ch - '0'), ch = getchar();return xx * ff;
}
void write(int out) {if (out < 0)putchar('-'), out = -out;if (out > 9)write(out / 10);putchar(out % 10 + '0');
}const int N= 80010;
struct data {int sz, lson, rson;
} tr[N * 500];
int to[N << 2], nxt[N << 2], nct, head[N], fas[N], fa[N][18], cnt, rt[N], cnt1,a[N], b[N], sz[N], dep[N], n, m, k, x, y, t, lastans;
char s[3];
bool vis[N];
void adde(int x, int y) {to[++nct] = y;nxt[nct] = head[x];head[x] = nct;
}
int find(int x)
{if (fas[x] == x) {return x;}return fas[x] = find(fas[x]);
}
void build(int &p, int l, int r) // 建树
{p = ++cnt;tr[p].sz = 0;if (l == r) {return;}int mid = (l + r) >> 1;build(tr[p].lson, l, mid);build(tr[p].rson, mid + 1, r);
}
void insert(int &p, int last, int l, int r, int x) {p = ++cnt;tr[p] = tr[last];tr[p].sz++;if (l == r) {return;}int mid = (l + r) >> 1;if (x <= mid) {insert(tr[p].lson, tr[last].lson, l, mid, x);} else {insert(tr[p].rson, tr[last].rson, mid + 1, r, x);}
}
void dfs(int u, int root, int p) // 建树
{fa[u][0] = p;for (int i = 1; i <= 17; i++) {fa[u][i] = fa[fa[u][i - 1]][i - 1];}vis[u] = 1;sz[root]++;dep[u] = dep[p] + 1;fas[u] = p;insert(rt[u], rt[p], 1, cnt1, lower_bound(b + 1, b + cnt1 + 1, a[u]) - b);for (int i = head[u]; i; i = nxt[i]) {int v = to[i];if (v != p) {dfs(v, root, u);}}
}
int get_lca(int x, int y) {if (dep[x] < dep[y]) {swap(x, y);}for (int i = 17; i >= 0; i--) {if (dep[fa[x][i]] >= dep[y]) {x = fa[x][i];}}if (x == y) {return x;}for (int i = 17; i >= 0; i--) {if (fa[x][i] != fa[y][i]) {x = fa[x][i];y = fa[y][i];}}return fa[x][0];
}
int query(int p, int hao1, int lca, int falca, int l, int r, int k) {if (l == r) {return b[l];}int mid = (l + r) / 2, midsz = tr[tr[p].lson].sz + tr[tr[hao1].lson].sz -tr[tr[lca].lson].sz -tr[tr[falca].lson].sz; if (midsz >= k) {return query(tr[p].lson, tr[hao1].lson, tr[lca].lson, tr[falca].lson, l,mid, k);} else {return query(tr[p].rson, tr[hao1].rson, tr[lca].rson, tr[falca].rson,mid + 1, r, k - midsz);}
}
signed main() {scanf("%d", &n);scanf("%d%d%d", &n, &m, &t);for (int i = 1; i <= n; i++) {scanf("%d", &a[i]);b[i] = a[i];fas[i] = i;}sort(b + 1, b + n + 1); // 离散化cnt1 = unique(b + 1, b + n + 1) - b - 1;for (int i = 1; i <= m; i++) {scanf("%d%d", &x, &y);adde(x, y);adde(y, x);}build(rt[0], 1, cnt1);for (int i = 1; i <= n; i++) {if (!vis[i]) {dfs(i, i, 0);fas[i] = i; // 重新确定并查集顶端元素,不然会TLE}}while (t--) // 操作{scanf("%s%d%d", s, &x, &y);x ^= lastans;y ^= lastans;if (s[0] == 'Q') {scanf("%d", &k);k ^= lastans;int lca = get_lca(x, y);printf("%d\n", lastans = query(rt[x], rt[y], rt[lca], rt[fa[lca][0]], 1,cnt1, k));} else {adde(x, y);adde(y, x);int xx = find(x), yy = find(y);if (sz[xx] < sz[yy]) {swap(xx, yy);swap(x, y);}dfs(y, xx, x);}}
}
扩展例题
www.luogu.com.cn
主席树学习笔记 - LPF’sBlog - 博客园
论 · 字符串
ac自动机 | [NOI2011] 阿狸的打字机
阿狸喜欢收藏各种稀奇古怪的东西,最近他淘到一台老式的打字机。打字机上只有 28 28 28 个按键,分别印有 26 26 26 个小写英文字母和 B
、P
两个字母。经阿狸研究发现,这个打字机是这样工作的:
-
输入小写字母,打字机的一个凹槽中会加入这个字母(这个字母加在凹槽的最后)。
-
按一下印有
B
的按键,打字机凹槽中最后一个字母会消失。 -
按一下印有
P
的按键,打字机会在纸上打印出凹槽中现有的所有字母并换行,但凹槽中的字母不会消失。
例如,阿狸输入 aPaPBbP
,纸上被打印的字符如下:
a
aa
ab
我们把纸上打印出来的字符串从 1 1 1 开始顺序编号,一直到 n n n。打字机有一个非常有趣的功能,在打字机中暗藏一个带数字的小键盘,在小键盘上输入两个数 ( x , y ) (x,y) (x,y)(其中 1 ≤ x , y ≤ n 1\leq x,y\leq n 1≤x,y≤n),打字机会显示第 x x x 个打印的字符串在第 y y y 个打印的字符串中出现了多少次。
阿狸发现了这个功能以后很兴奋,他想写个程序完成同样的功能,你能帮助他么?
对于 100 % 100\% 100% 的数据, 1 ≤ n ≤ 1 0 5 1\leq n\leq 10^5 1≤n≤105, 1 ≤ m ≤ 1 0 5 1\leq m\leq10^5 1≤m≤105,第一行总长度 ≤ 1 0 5 \leq 10^5 ≤105。
测试点 | n n n 的规模 | m m m 的规模 | 字符串长度 | 第一行长度 |
---|---|---|---|---|
1 , 2 1,2 1,2 | 1 ≤ n ≤ 100 1\leq n\leq 100 1≤n≤100 | 1 ≤ m ≤ 1 0 3 1\leq m\leq 10^3 1≤m≤103 | - | ≤ 100 \leq 100 ≤100 |
3 , 4 3,4 3,4 | 1 ≤ n ≤ 1 0 3 1\leq n\leq 10^3 1≤n≤103 | 1 ≤ m ≤ 1 0 4 1\leq m\leq 10^4 1≤m≤104 | 单个长度 ≤ 1 0 3 \leq 10^3 ≤103,总长度 ≤ 1 0 5 \leq 10^5 ≤105 | ≤ 1 0 5 \leq 10^5 ≤105 |
5 ∼ 7 5\sim 7 5∼7 | 1 ≤ n ≤ 1 0 4 1\leq n\leq 10^4 1≤n≤104 | 1 ≤ m ≤ 1 0 5 1\leq m\leq 10^5 1≤m≤105 | 总长度 ≤ 1 0 5 \leq 10^5 ≤105 | ≤ 1 0 5 \leq 10^5 ≤105 |
8 ∼ 10 8\sim 10 8∼10 | 1 ≤ n ≤ 1 0 5 1\leq n\leq 10^5 1≤n≤105 | 1 ≤ m ≤ 1 0 5 1\leq m\leq 10^5 1≤m≤105 | - | ≤ 1 0 5 \leq 10^5 ≤105 |
哇噻!自动ac耶!真的可以吗?
ac自动机,用于多模态字符串匹配。ACAM
如果我们要多次询问一个单词在文本中出现的次数/在几个单词中的子串有出现怎么办?AC自动机帮您AC!
首先对于字符串匹配,要明确两个概念
-
模式串:需要在文本串中寻找的子串
-
匹配串(文本串):原文本串
如果说是统计一个单词在所有单词中的出现次数(完整匹配或者说子串匹配),我们是把所有的文本串都构建在一颗Trie树上,然后拿着模式串在Trie树上跑。但是AC自动机不同——它的把所有模式串构建出一颗Trie树,然后从前往后遍历文本串,快速地在Trie树上不同的模式串之间跳转,来统计出每一个模式串出现的次数。
当模式串很多时,AC自动机就遥遥领先了。
好吧,白高兴一场……可是应该怎么样构建AC自动机呢?
构建AC自动机嘛……请看说明:
-
构造自动机在 Trie 上构建两类边:回跳边和转移边。
-
构建Fail指针
-
扫描文本串:双指针,一个指针指向Trie树,一个指针指向文本串,同步转移并且统计答案
等等,回跳边和转移边是什么?
看看下面的就知道了。其实回跳边和转移边就是一样的东西,就是Fail指针。都是在失配(或者匹配成功)时快速跳到另一个模式串满足原串是模式串的前缀,即也匹配了一些前缀部分省的指针往回走浪费复杂度
给我模板!!
//AC自动机模板#include<bits/stdc++.h>
using namespace std;const int N=1e6+5;
int n,x,t,i,cnt=1,vis[200],ans;
char c[N][100];
struct node {int nxt[27];int fail,flag;void init(){fail=flag=0;memset(nxt,0,sizeof nxt);}
} trie[N];void add(char s[],int num) {往Trie数中加入字符串;
}
queue<int> q;
void getFail() { //求出Fail指针广搜求出Fail指针;push rt->qwhile(q.size)q->popfor each son:create Failson? have:son->qempty:create Fail
}signed main() {输入模式串,往Trie中加入模式串;处理Fail指针;双指针遍历文本串,求出答案;
}
拓扑建图优化
让我们把Trie上的fail都想象成一条条有向边,那么我们如果在一个点对那个点进行一些操作,那么沿着这个点连出去的点也会进行操作(就是跳fail),所以我们才要暴力跳fail去更新之后的点。
论 · 运用能力
运用能力很重要!!
论 · 优化思想
莫队 | [国家集训队] 小 Z 的袜子
作为一个生活散漫的人,小 Z 每天早上都要耗费很久从一堆五颜六色的袜子中找出一双来穿。终于有一天,小 Z 再也无法忍受这恼人的找袜子过程,于是他决定听天由命……
具体来说,小 Z 把这 N N N 只袜子从 1 1 1 到 N N N 编号,然后从编号 L L L 到 R R R 的袜子中随机选出两只来穿。尽管小 Z 并不在意两只袜子是不是完整的一双,他却很在意袜子的颜色,毕竟穿两只不同色的袜子会很尴尬。
你的任务便是告诉小 Z,他有多大的概率抽到两只颜色相同的袜子。当然,小 Z 希望这个概率尽量高,所以他可能会询问多个 ( L , R ) (L,R) (L,R) 以方便自己选择。
然而数据中有 L = R L=R L=R 的情况,请特判这种情况,输出0/1
。
100 % 100\% 100% 的数据中, N , M ≤ 50000 N,M \leq 50000 N,M≤50000, 1 ≤ L < R ≤ N 1 \leq L < R \leq N 1≤L<R≤N, C i ≤ N C_i \leq N Ci≤N。
莫队?什么队?莫又是什么?
莫队,就是用来优化暴力转移过程的离线算法。莫:莫涛。
解题一句话:首先考虑最暴力的转移,还是简单的,然后我们套上莫队即可。
莫队的精髓就在于“尽可能地多利用上一个询问获取的数据”,可以理解为在多次查询区间信息时,构造一种询问的顺序使得双指针运动的距离和最小。
那么实际上双指针运动的距离就是这个算法的时间复杂度,而利用莫队我们可以把最劣复杂度到达O(n^2)的数据降低至O(n\sqrt n),这在某些场合无疑是巨大的突破。
那么具体来说怎么做呢?继续看。
玄学优化?无厘头?
莫队,需要把询问分块。在普通写法中,双指针l,r会漫无目的乱飞,所以时间复杂度不优。但是莫队的询问分块采用如下策略
-
按询问的l分块,并把同一块的询问放在一起
-
对于同一块内的询问,按询问的r排序(注意排序后r还是散落在整个序列中,不过是升序的,不会来回跳)
这样有什么用呢?我们发现这样所有询问的r一定升序,并且l在每一块内都不会运动很远,所以综合时间复杂度更加优秀。
进一步优化,我们发现从一块跳到下一块时,r在比较靠后的位置。但是从下一块开始r又是从头开始递增的,r又要跑回去再跑一边过来。所以我们可以优化——让第二个分块内的r按降序排列,这样就可以在跑回去的过程中解决了。
于是乎就有奇偶性排序,即
-
奇数(编号的)块的r按升序排序
-
偶数(编号的)块的r按降序排序
知道了,快给沃模板!
模板如下
int ;//记录答案的数组
struct query{int id,l,r;//记录询问
}int getKid(int x){//获取x对应的块序号(x/块长)
}bool cmp(query a,query b){//排序
} void del(int x,int &res){//处理从双指针范围内剔除一个数(如l++、r--时),这和暴力时是一样的只不过封装一下
}void add(int x,int &res){//处理在双指针范围内添加一个数(如l--、r++时)
}signed main (){读入询问;确定块长;对询问排序;按排序后的顺序遍历,双指针求解,和暴力无异;输出答案;
}
例题 Code
/*
CB Ntsc111
*/#include <bits/stdc++.h>
using namespace std;#define ull unsigned int
#define pii pair<int, int>
#define pf to
#define ps second
#define int long long#define err cerr << "Error"
#define rd read()#define ot write
#define nl putchar('\n')
int read() {int xx = 0, ff = 1;char ch = getchar();while (ch < '0' || ch > '9') {if (ch == '-')ff = -1;ch = getchar();}while (ch >= '0' && ch <= '9')xx = xx * 10 + (ch - '0'), ch = getchar();return xx * ff;
}
void write(int out) {if (out < 0)putchar('-'), out = -out;if (out > 9)write(out / 10);putchar(out % 10 + '0');
}const int mxxlog = 10;
int MOD = 1e18 + 7;
const int N = 1e6 + 9;struct node{int l,r,len,id;
}b[N];int a[N],bl[N],n,m,B,num[N];
int ans1[N],ans2[N];
int tot;bool cmp(const node &a, const node &b) { if (bl[a.l]<bl[b.l])return 1;if(bl[a.l]>bl[b.l])return 0;if(bl[a.l]&1)return a.r<b.r;return a.r>b.r;}void add(int x){tot+=((num[x]<<1)|1),num[x]++;
}void del(int x){tot+=1-((num[x]<<1)),num[x]--;
}signed main() {n = rd, m = rd;int B = n / sqrt((m >> 1) / 3);for (int i = 1; i <= n; i++)a[i] = rd;for (int i = 1; i <= m; i++) {b[i].l = rd, b[i].r = rd, b[i].len = b[i].r - b[i].l + 1;b[i].id=i,ans2[i]=1,bl[i]=(i-1)/B+1;}sort(b + 1, b + m + 1, cmp);int l = b[1].l, r = b[1].r;for (int i = l; i <= r; i++)add(a[i]);int fz, fm, g;if (tot != b[1].len) {fz = tot - b[1].len, fm = b[1].len * (b[1].len - 1), g = __gcd(fz, fm);ans1[b[1].id] = fz / g, ans2[b[1].id] = fm / g;}for (int i = 2; i <= m; i++) {while (l < b[i].l)del(a[l++]);while (l > b[i].l)add(a[--l]);while (r > b[i].r)del(a[r--]);while (r < b[i].r)add(a[++r]);if (b[i].len != tot) {fz = tot - b[i].len, fm = b[i].len * (b[i].len - 1), g = __gcd(fz, fm);ans1[b[i].id] = fz / g, ans2[b[i].id] = fm / g;}}for (int i = 1; i <= m; i++) {cout << ans1[i] << '/' << ans2[i] << endl;}
}
结
喜欢(玩原神)的你。