特点与二分对比
-
和二分类似,也是加速枚举过程。
-
不同之处:倍增通常需要预处理一些东西,预处理复杂度高,判断合法性复杂度低。二分则相反。
ST表
- ST表是一种很好的反应倍增思想的数据结构,不仅限于维护区间内的最大值,下面例题(
坑了半天的紫题)可以很好的体现出这一点。
用一个 f[ i ][ j ] 二位数组来维护区间[ i, i + 2^j - 1 ]的最大值,其预处理方法与树上 LCA 类似,不再赘述。
释其对于某个区间的查询功能还是有必要的:
计算出log2 |S|,S=r - l + 1,然后对于左端点和右端点分别进行查询:
int Query(int l,int r)
{
int c=log2(r-l+1);
return max(st[l][c],st[r-(1<<c)+1][c]);//把拆出来的区间分别取最值
}
代码如下:类似于区间 dp
#include <iostream>
#include <cstdio>
#include <cmath>
#define re register
using namespace std;
const int maxn=1e6 + 10;
inline int read()
{
int x=0,f=1;char ch=getchar();
while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
while (isdigit(ch)){x=x*10+ch-48;ch=getchar();}
return x*f;
}
int f[maxn][21];
int n,m;
inline int query(int l,int r){
int c=log2(r-l+1);
return max(f[l][c],f[r-(1<<c)+1][c]);
}
int main(){
//freopen("1.in","r",stdin);
n=read();m=read();
for(re int i=1;i<=n;i++)f[i][0]=read();//他自己
for(re int j=1;j<=20;j++)//状态
for(re int i=1;i+(1<<j)-1<=n;i++){
f[i][j]=max(f[i][j-1],f[i+(1<<j-1)][j-1]);
}
for(re int i=1;i<=m;i++){
int l=read(),r=read();
printf("%d\n",query(l,r));
}
return 0;
}
例题 NOI2010超级钢琴
题目大意
给你一个长度为n的序列a1,a2,a3...an,找出 k个子序列使得这 k 个子序列的和最大,且这 k 个子序列的长度在l 到 r之间。
题目分析
这是一道非常经典的 st 表优化的题,闭上眼睛想一想:为什么用 st 表??怎么优化??
-
要求最大子序列,显然要用到前缀和,这是一个思考方向。
-
为什么用 st 表?因为出现了负数。前缀和肯定不是一直增大的,我们如果单纯暴力算出最大字段和会造成极大的时间浪费,借助st表的功能,因此st 表维护的是前缀和的最大值。
下面怎么求解??
采用问题简化的思想(做题即翻译,先往最简单的方面想):
我们固定一个起点i,怎么求以它为开头的最大字段和??
显然是找到一个k点,使得sum[k]-sum[i-1]最大化,这不就是st表维护和查询的功能的典型体现吗?但是st表查询是要有区间限制的,否则这题也没意义(想一想,为什么?)。不难看出,k的取值范围是,[i+l-1,i+r-1],但是这里有一个细节,i+r-1要和n取最小值。
有了思路,怎么实现?
- 依据上面的思路,我们需要用st表查询最大前缀和,从而求出最大字段和,因此初始化st表就简单了
for(re int i=1;i<=n;i++)f[i][0]=sum[i];
for(re int j=1;j<=19;j++)
for(re int i=1;i<=n;i++){
d[i][j]=d[d[i][j-1]+1][j-1];
f[i][j]=max(f[i][j-1],f[d[i][j-1]+1][j-1]);
}
能有这么简单?我很快就到你家门口
我们把此时的答案记作f(id,l,r),表示以 id 为起点的到l到r区间某个点 k 的最大字段和,满足了题意求一个的目的。这个普通的RMQ有什么区别呢?在于我们需要知道我们取最大值的位置,不然我们会不知道我们取了哪一串音符,有可能会取重或者取不到(这是一个理由)。
- 因此记录最大值出现的位置
for(re int j=1;j<=19;j++)
for(re int i=1;i<=n;i++){
d[i][j]=d[d[i][j-1]+1][j-1];//类似于LCA倍增
if(f[i][j-1]>f[d[i][j-1]+1][j-1]){
f[i][j]=f[i][j-1];
id[i][j]=id[i][j-1];
}
else{
f[i][j]=f[d[i][j-1]+1][j-1];
id[i][j]=id[d[i][j-1]+1][j-1];
}
}
- 查询操作
inline int query(int L,int R){//???
int maxx=-INF,pos;
for(re int j=19;j>=0;j--){
if(L+(1<<j)-1<=R){
if(f[L][j]>maxx)maxx=f[L][j],pos=id[L][j];
L=d[L][j]+1;
}
}
return pos;
}
更重要的是下面的,会求一个,多个怎么求??需要用到大根堆处理这个问题,用优先队列实现,以sum[ k ] - sum[ id-1 ]的值重载小于号,表示f(id,l,r)的值。
首先我们可以将 n 个四元组f(id,i,j,nw)加入优先队列中,此时我们先取出一个队顶元素,这肯定是全局最大的值了,如何找到第二大的呢?
- 我们可以先破开最大的这一个,将其分成两段:需要特判
1.(id,l,nw-1,f(id,l,nw-1)取到的最大值 k)
2.(id ,nw+1,r,f(id,nw+1,r)取到的最大值 k)
取出 m 次后,ans更新,得到答案。
#include <iostream>
#include <cstdio>
#include <queue>
#define re register
using namespace std;
const int maxn=5e5 + 10,INF=2e9;
int n,k,l,r,sum[maxn],f[maxn][20],id[maxn][20],d[maxn][20];//d是辅助空间
long long ans;
struct node{
int id,l,r,nw;
bool operator <(const node &x)const{
return sum[nw]-sum[id-1]<sum[x.nw]-sum[x.id-1];
}
};
priority_queue <node> q;
inline int query(int L,int R){//???
int maxx=-INF,pos;
for(re int j=19;j>=0;j--){
if(L+(1<<j)-1<=R){
if(f[L][j]>maxx)maxx=f[L][j],pos=id[L][j];
L=d[L][j]+1;
}
}
return pos;
}
int main(){
//freopen("1.in","r",stdin);
scanf("%d%d%d%d",&n,&k,&l,&r);
for(re int i=1;i<=n;i++){
int tmp;scanf("%d",&tmp);
sum[i]=sum[i-1]+tmp;
d[i][0]=id[i][0]=i;
f[i][0]=sum[i];//针对谁建st表谁就是初始化值
}
for(re int j=1;j<=19;j++)
for(re int i=1;i<=n;i++){
d[i][j]=d[d[i][j-1]+1][j-1];//类似于LCA倍增
if(f[i][j-1]>f[d[i][j-1]+1][j-1]){
f[i][j]=f[i][j-1];
id[i][j]=id[i][j-1];
}
else{
f[i][j]=f[d[i][j-1]+1][j-1];
id[i][j]=id[d[i][j-1]+1][j-1];
}
}
for(re int i=1;i<=n-l+1;i++){//注意细节
node tmp;
tmp.id=i;
tmp.nw=query(i+l-1,min(n,i+r-1));//又一个细节
tmp.l=i+l-1;tmp.r=min(n,i+r-1);
q.push(tmp);
}
while(k--){//拆!
node t=q.top();q.pop();node tmp;
ans+=sum[t.nw]-sum[t.id-1];
if(t.nw>t.l){//左
tmp.l=t.l;tmp.r=t.nw-1;
tmp.id=t.id;//起点是不变的
tmp.nw=query(tmp.l,tmp.r);
q.push(tmp);
}
if(t.nw<t.r){
tmp.l=t.nw+1;tmp.r=t.r;
tmp.id=t.id;
tmp.nw=query(tmp.l,tmp.r);
q.push(tmp);
}
}
printf("%lld",ans);
return 0;
}
例题,树上倍增
题目大意(做题即翻译)
-
给出你 n 个点的无向图,以及 m 条已确定的带有权值的边。
-
对于任意的一个三元组( i , j , k),如果 dis[ i ][ j ]< dis[ i ][ k ] and dis[ i ][ j ] < dis[ j ][ k ],就不好。
-
让你更改其余所有边的权值,使得这个图好起来,并且权值和最小(应该是个完全图,任意两点间都有连边)
分析
容易得到,如果对于任意的三元组已经确定了两条边的权值为a,b,那么第三条边的权值必为 min ( a , b )。(这是从题目最本质的点出发)
继续把上面结论推广,如果在图中存在一条从 u , 到 v 的路径,那么 dis[ u ][ v ] 必为路径上的最小权值,否则就会出现题目中的无解状态,输出 -1 即可。
我们来看原题的样例二:很明显这就是个无解状态。
所以怎么来求最小权值和呢?
先考虑已经给出的 m 条边,他们的权值都是确定的。
再说剩下的,我们称作不在同一个连通分量里的边,直接连权值 1 的边是没有任何问题的(可以举反例)。那么所有的边就考虑完了,但是基于要统计所有的 min ,我们不可能枚举每两个点来统计,会 T 飞。
闭上眼睛想一想:这无妨就是一个求最大生成树的过程
-
那些还无法确定的边(在同一个连通分量里,不要和上面的连 1 搞混)等于这棵树上两个点路径上最小的那条边最优
-
而对于那些没有加入这棵生成树中的确定边,必须等于这条路径上的最小边,否则非法直接-1。换句话说,考虑一条边(u ,v , w ),u 到 v 的所有路径中的最小值必定等于w,否则输出 -1,无解。
倍增思想就运用在了维护最小值上
//from kupi
#include <algorithm>
#include <cstdio>
using namespace std;
typedef long long ll;
int const maxn = 300003, inf = 0x3f3f3f3f;
struct Edge {
int x, y, z;
};
inline bool operator<(Edge const &lhs, Edge const &rhs) { return lhs.z > rhs.z; }
int n = 0, m = 0;
Edge a[maxn];
int head[maxn], nxt[maxn << 1], to[maxn << 1], val[maxn << 1], cnt = 0;
inline void insert(int u, int e, int v) {
nxt[++cnt] = head[u];
head[u] = cnt;
to[cnt] = e;
val[cnt] = v;
}
namespace calc {
int fa[maxn], siz[maxn], mi[maxn];
inline int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); }
ll Kruskal() {
sort(a + 1, a + m + 1);
for (int i = 1; i <= n; ++i) {
fa[i] = i;
siz[i] = 1;
mi[i] = inf;
}
ll counted = 0, sum = 0;
for (int i = 1; i <= m; ++i) {
int x = find(a[i].x), y = find(a[i].y);
if (x != y) {
sum += ll(siz[x]) * siz[y] * a[i].z;
counted += ll(siz[x]) * siz[y];
if (siz[x] < siz[y]) std::swap(x, y);
fa[y] = x;
siz[x] += siz[y];
mi[x] = a[i].z;
insert(a[i].x, a[i].y, a[i].z);
insert(a[i].y, a[i].x, a[i].z);
} else if (mi[x] > a[i].z)
return -1;
}
sum += (ll(n) * (n - 1ll) / 2 - counted);
return sum;
}
} // namespace calc
namespace check {
bool vis[maxn];
int dep[maxn], fa[19][maxn], mi[19][maxn];
void dfs(int x) {
vis[x] = true;
for (int j = 1; j <= 18; ++j) {
fa[j][x] = fa[j - 1][fa[j - 1][x]];
mi[j][x] = min(mi[j - 1][x], mi[j - 1][fa[j - 1][x]]);
}
for (int i = head[x]; i; i = nxt[i])
if (!vis[to[i]]) {
dep[to[i]] = dep[x] + 1;
fa[0][to[i]] = x;
mi[0][to[i]] = val[i];
dfs(to[i]);
}
}
int query_min(int x, int y) {
if (dep[x] < dep[y]) swap(x, y);
int ans = inf;
for (int j = 18; ~j; --j)
if (dep[fa[j][x]] >= dep[y]) {
ans = min(ans, mi[j][x]);
x = fa[j][x];
}
if (x == y) return ans;
for (int j = 18; ~j; --j)
if (fa[j][x] != fa[j][y]) {
ans = min(ans, min(mi[j][x], mi[j][y]));
x = fa[j][x];
y = fa[j][y];
}
ans = min(ans, min(mi[0][x], mi[0][y]));
return ans;
}
bool check() {
for (int i = 1; i <= n; ++i)
if (!vis[i]) {
fa[0][i] = i;
mi[0][i] = inf;
dep[i] = 1;
dfs(i);
}
for (int i = 1; i <= m; ++i) {
int v = query_min(a[i].x, a[i].y);
if (v != a[i].z) return false;
}
return true;
}
}
int main() {
scanf("%d %d", &n, &m);
for (int i = 1; i <= m; ++i)
scanf("%d %d %d", &a[i].x, &a[i].y, &a[i].z);
ll ans = calc::Kruskal();
if (ans == -1 || !check::check())
printf("-1");
else
printf("%lld", ans);
return 0;
}
例题 NOIP2012 开车旅行
题目大意(做题即翻译)
给你一个已知的有向图,每个结点有一个编号即高度,从西到东排列且只能从西到东走,任意两点间的距离为他俩点的高度差的绝对值。
现在有两个人要轮流开车,任选一个起点 S 出发,A 开一天车,B 开一天车。不同的是,A 都会开往第二近的地方,B 都会开往最近的地方。
问你两个问题:给出你最大行驶距离 x0 ,从哪个点开始走 la / lb 最小(la,lb是他俩开车分别走的距离),第二个问题和第一个类似,求出 la ,lb 即可。
分析
不难发现,处理出每个点的最近城市和次近城市是有必要的,可以用平衡树来维护:
-
一个点的最近城市无非就是他在平衡树里的前驱或后继
-
次近城市有两种情况:
-
1.最近城市是前驱结点,那么次近城市就是前驱的前驱和后继之一。
-
2.最近城市是后继结点,那么次近城市就是前驱和后继的后继之一。
这里有两个小细节:
1. 由于只能从小往大开,因此要从 n 到 1倒序遍历更新。
2. 在处理 n 的时候可能越界,需要插入两个最大值和两个最小值:h[ 0 ]=INF , h[ n + 1 ] = -INF(想一想,为什么)
因此维护功能就写出来了
h[0]=INF,h[n+1]=-INF;
node st;
st.id=0,st.al=INF;
q.insert(st),q.insert(st);
st.id=n+1,st.al=-INF;
q.insert(st),q.insert(st);
for(int i=n;i;i--){
int ga,gb;
node now;
now.id=i,now.al=h[i];
q.insert(now);
set<node>::iterator p=q.lower_bound(now);
p--;
int lt=p->id,lh=p->al;//前驱
p++,p++;
int nt=p->id,nh=p->al;//后继
p--;//回到初始位置
if(abs(lh-h[i])<=abs(nh-h[i])){//前驱最近
gb=lt;
p--,p--;
if(abs(p->al-h[i])<=abs(nh-h[i])){//前驱的前驱
ga=p->id;
}
else ga=nt;
}
else{//后继最近
gb=nt;
p++,p++;
if(abs(p->al-h[i])>=abs(lh-h[i])){
ga=lt;
}
else ga=p->id;
}
}
这里用到了set维护最大和次大值的思想,非常重要。
回到问题上,得到了每个点的最近和次近结点,肯定是要记录的,偷偷看一眼标签,是倍增!因此需要用一个 f(i , j , k)k 先开车存储从 i 号结点走 2^j 次到达的城市结点,得到如下的初始化。同理,采用倍增思想,da db分别表示 A 和 B 的开车距离
f[i][0][0]=ga,f[i][0][1]=gb;
da[i][0][0]=abs(h[ga]-h[i]);
db[i][0][1]=abs(h[gb]-h[i]);
怎么转移?又一个细节,i = 1时,前半段开车和后半段开车的不是一个人:
if(j==1){
f[i][1][k]=f[i][0][k]+f[f[i][0][k]][0][1-k];
da[i][1][k]=da[i][0][k]+da[f[i][0][k]][0][1-k];
db[i][1][k]=db[i][0][k]+db[f[i][0][k]][0][1-k];
}
else if(j>1){
f[i][j][k]=f[i][j-1][k]+f[f[i][j-1][k]][j-1][k];
da[i][j][k]=da[i][j-1][k]+da[f[i][j-1][k]][j-1][k];
db[i][j][k]=db[i][j-1][k]+db[f[i][j-1][k]][j-1][k];
}
因此问题解决了一大半。
怎么模拟行进过程?与倍增的查询是类似的。
void work1(int S,int X){
int p=S;
la=0;lb=0;
for(int i=18;i>=0;i--){//a先开车
if(f[p][i][0]&&la+lb+da[p][i][0]+db[p][i][0]<=X){//不能越界
la+=da[p][i][0];
lb+=db[p][i][0];
p=f[p][i][0];
}
}
}
问题一和二的本质是一样的,不知道你发现了没有?
完整代码:
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <set>
#include <cstdlib>
using namespace std;
const int maxn=1e5+200,INF=2e9;
struct node{
int id,al;
bool operator <(const node &x)const{
return al<x.al;//按海拔来
}
};
int n,m,x0,la,lb;
int h[maxn],s[maxn],x[maxn];
int f[maxn][20][3],da[maxn][20][3],db[maxn][20][3];
double ans=INF*1.0;//控制精度
multiset <node> q;
void prework(){
h[0]=INF,h[n+1]=-INF;
node st;
st.id=0,st.al=INF;
q.insert(st),q.insert(st);
st.id=n+1,st.al=-INF;
q.insert(st),q.insert(st);
for(int i=n;i;i--){
int ga,gb;
node now;
now.id=i,now.al=h[i];
q.insert(now);
set<node>::iterator p=q.lower_bound(now);
p--;
int lt=p->id,lh=p->al;//前驱
p++,p++;
int nt=p->id,nh=p->al;//后继
p--;//回到初始位置
if(abs(lh-h[i])<=abs(nh-h[i])){//前驱最近
gb=lt;
p--,p--;
if(abs(p->al-h[i])<=abs(nh-h[i])){//前驱的前驱
ga=p->id;
}
else ga=nt;
}
else{//后继最近
gb=nt;
p++,p++;
if(abs(p->al-h[i])>=abs(lh-h[i])){
ga=lt;
}
else ga=p->id;
}
f[i][0][0]=ga,f[i][0][1]=gb;
da[i][0][0]=abs(h[ga]-h[i]);
db[i][0][1]=abs(h[gb]-h[i]);
}
for(int i=1;i<=18;i++)
for(int j=1;j<=n;j++)
for(int k=0;k<2;k++){
if(i==1){
f[j][1][k]=f[f[j][0][k]][0][1-k];
da[j][1][k]=da[j][0][k]+da[f[j][0][k]][0][1-k];
db[j][1][k]=db[j][0][k]+db[f[j][0][k]][0][1-k];
}
else{
f[j][i][k]=f[f[j][i-1][k]][i-1][k];
da[j][i][k]=da[j][i-1][k]+da[f[j][i-1][k]][i-1][k];
db[j][i][k]=db[j][i-1][k]+db[f[j][i-1][k]][i-1][k];
}
}
}
void work1(int S,int X){
int p=S;
la=0;lb=0;
for(int i=18;i>=0;i--){//a先开车
if(f[p][i][0]&&la+lb+da[p][i][0]+db[p][i][0]<=X){//不能越界
la+=da[p][i][0];
lb+=db[p][i][0];
p=f[p][i][0];
}
}
}
int ansid;
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",h+i);
scanf("%d%d",&x0,&m);
for(int i=1;i<=m;i++)scanf("%d%d",s+i,x+i);
prework();
//for(int i=1;i<=n;i++)printf("%d %d %d\n",f[i][0][0],da[i][0][0],db[i][0][1]);
//system("pause");
for(int i=1;i<=n;i++){
work1(i,x0);
double nowans=(double)la/(double)lb;
if(nowans<ans){
ans=nowans;
ansid=i;
}
else if(nowans==ans&&h[ansid]<h[i]){
ansid=i;
}
}
printf("%d\n",ansid);
for(int i=1;i<=m;i++){
work1(s[i],x[i]);
printf("%d %d\n",la,lb);
}
return 0;
}