0%

[IOI2018] 会议

最近打模拟赛遇到的,不得不说非常神仙。

又难打又难调,写篇题解纪念一下。

题目链接:luoguP5044

Subtask2

我们先来看这个部分分。

这是一个比较显然的区间 dp。(虽然我在模拟赛时并没有看出来

为区间 的最优解, 为这个区间内任何一个最大值的位置

时,显然

否则这个方程有两种转移方式。

时,有转移

时,有转移

时,会议在 召开显然不会最优,所以忽略。

其中第一种方式是会议地址在最大值左边的情况,第二种方式会议地址在最大值右边。

因为会议地址在最大值一边时,另一边所有点的代价显然都是区间最大值。

并且显然会议地址只能有上述两种方式。

所以这种dp是正确的。

快乐的 dp。

喜提 分。

正解

我们发现这个 dp 与区间最大值的位置有关系,所以考虑在笛卡尔树进行 dp。

我们首先可以把 st 表和笛卡尔树建出来(因为 不算特别大,所以还是可以 进行建树的)。

首先我们可以想到的思路是对于笛卡尔树上的每一个节点,我们将它所对应区间的 dp 值求出来。

但是会发现,虽然这样子是可以预处理的,但是无法处理询问,因为如果数列是递增/递降的,笛卡尔树高就会为

并且我们在每一次询问的区间不一定对应着笛卡尔树上恰好一个节点,这个区间很可能是由许多节点“拼起来”的。

那么我们对于每一个询问就要访问这个区间的所有节点,极限情况下要访问 个节点。

时间复杂度为

所以我们只能考虑别的方法。

首先我们根据上面的结论:若区间值不唯一,则最优解的位置一定不在区间最大值。

那么我们可以将一个询问拆成两个:一个是最优解在最大值左边的情况,一个是最优解在最大值右边的情况。(最大值选任何一个)

那么这两个询问显然可以通过同一种方式来处理,我们这里主要讲解第二种询问。

第一种询问可以通过将这个序列翻转,然后再做一遍。

那么拆出的这个询问和原来的询问有什么区别呢?

其他地方没什么不同,但是拆出的这个询问多了一个条件——这个区间左边的值大于等于询问区间内所有数的值。

那么这个时候就会有一个新的可行思路:

假设当前节点所对应的下标为 (也就是对应区间最大值的位置),所对应的区间为

那么我们能不能将所有的 求出来呢?

现在我们假设这个节点的左右儿子的 值都处理完毕,考虑合并 值。

对于 ,这部分dp只已经在左儿子里求出来了,不用管。

对于 ,有 。(因为最优解一定不在区间最大值)

对于 ,有

这个 的第一个值表示会议地址在最大值左边,第二个值表示会议地址在最大值右边。

分做法的 dp 方程差不多。

好像也不好做啊。

我们再来仔细观察,就会发现当 时一定会有 。因为一定有 此时就算会议地址和原来选一个,代价也只会增加

再来观察上面的 方程。随着 的增加,左边的值会增加 ,而右边的值增加量一定小于等于

这告诉我们对于这个区间,一定是左边的一部分用第一种决策,剩下的部分都用第二种决策。

所以我们可以二分出这两种决策的分界线。

那么如何转移呢?

对于第二种转移,需要在原来的基础上区间加一个值;对于第一种转移,则需要先区间赋 ,再加上一个一次函数。

再结合之前二分的需求,我们就可以用一个支持区间赋值、区间增加一次函数(假设常数也算一次函数)的线段树来解决问题。

最后询问只需要在拆出的两次询问中选最小值即可。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<vector>
using namespace std;
using pi=pair<int,int>;
using ll=long long;
inline int read()
{
int s=0,w=1;char ch;
while((ch=getchar())>'9'||ch<'0') if(ch=='-') w=-1;
while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar();
return s*w;
}
vector<pi>q[750001];
int st[750001][21];
ll tagk[3000001];
ll tagb[3000001];
ll tagf[3000001];
ll num[3000001];
int mid[750001];
ll ans[750001];
int l[750001];
int r[750001];
int h[750001];
int p[750001];
bool flag;
int root;
int n,m;
inline int get_max(int l,int r)
{
int num=p[r-l],val1=st[l][num],val2=st[r-(1<<num)+1][num];
// printf("get_max %d %d %d : %d\n",l,r,num,h[val1]>h[val2]?val1:val2);
if(!flag) return h[val1]>h[val2]?val1:val2;
return h[val1]>=h[val2]?val1:val2;
}
void run_st()
{
for(int i=n;i;i--) for(int j=1;i+(1<<j)-1<=n;j++)
st[i][j]=get_max(i,i+(1<<j)-1);
}
void push_down(int p,int pl,int pr)
{
int mid=(pl+pr)>>1;
if(~tagf[p])
{
tagk[p*2]=tagk[p*2|1]=tagb[p*2]=tagb[p*2|1]=0;
tagf[p*2]=tagf[p*2|1]=num[p*2]=num[p*2|1]=tagf[p];
tagf[p]=-1;
}
num[p*2]+=mid*tagk[p]+tagb[p];
num[p*2|1]+=pr*tagk[p]+tagb[p];
tagk[p*2]+=tagk[p],tagk[p*2|1]+=tagk[p],tagk[p]=0;
tagb[p*2]+=tagb[p],tagb[p*2|1]+=tagb[p],tagb[p]=0;
}
void push_up(int p)
{
num[p]=num[p*2|1];
}
void cover(int p,int pl,int pr,int l,int r,ll y)
{
if(l<=pl&&pr<=r){ tagk[p]=tagb[p]=0,tagf[p]=num[p]=y;return ;}
push_down(p,pl,pr);
int mid=(pl+pr)>>1;
if(l<=mid) cover(p*2,pl,mid,l,r,y);
if(mid<r) cover(p*2|1,mid+1,pr,l,r,y);
push_up(p);
}
ll get(int p,int pl,int pr,int x)
{
if(pl==pr) return tagf[p]+tagk[p]*pl+tagb[p];
push_down(p,pl,pr);
int mid=(pl+pr)>>1;
if(x<=mid) return get(p*2,pl,mid,x);
else return get(p*2|1,mid+1,pr,x);
}
int cal(int p,int pl,int pr,int l,int r,int len,ll dp)
{
if(pl==pr)
{
if(dp+(ll)(pl-l+1)*h[l-1]<=num[p]+(ll)len*h[l-1]) return pl;
return pl-1;
}
push_down(p,pl,pr);
int mid=(pl+pr)>>1;
if(l>mid) return cal(p*2|1,mid+1,pr,l,r,len,dp);
if(mid>=r) return cal(p*2,pl,mid,l,r,len,dp);
if(dp+(ll)(mid-l+1)*h[l-1]<=num[p*2]+(ll)len*h[l-1]) return cal(p*2|1,mid+1,pr,l,r,len,dp);
return cal(p*2,pl,mid,l,r,len,dp);
}
void add(int p,int pl,int pr,int l,int r,ll k,ll b)
{
// printf("add %d %d %d %d %d %lld %lld : %lld\n",p,pl,pr,l,r,k,b,num[p]);
if(l<=pl&&pr<=r)
{
tagk[p]+=k,tagb[p]+=b;
num[p]+=k*pr+b;
return ;
}
push_down(p,pl,pr);
int mid=(pl+pr)>>1;
if(l<=mid) add(p*2,pl,mid,l,r,k,b);
if(mid<r) add(p*2|1,mid+1,pr,l,r,k,b);
push_up(p);
}
void dfs(int nl,int nr)
{
int num=get_max(nl,nr),nb=-1;ll dp;
if(num!=nl)
{
dfs(nl,num-1);
dp=get(1,1,n,num-1)+h[num];
}
else dp=h[num];
cover(1,1,n,num,num,dp);
if(num!=nr)
{
dfs(num+1,nr);
int mi=nb=cal(1,1,n,num+1,nr,num-nl+1,dp);
if(mi>num)//(num,mi]
{
cover(1,1,n,num+1,mi,0);
add(1,1,n,num+1,mi,h[num],dp-(ll)num*h[num]);
}
if(mi<nr)//(mi,nr]
add(1,1,n,mi+1,nr,0,(ll)(num-nl+1)*h[num]);
}
// printf("dfs %d %d %d %d\n",nl,nr,num,nb);
for(auto i:q[num])
{
int id=i.first,qr=i.second;
ans[id]=min(ans[id],get(1,1,n,qr)+(ll)(mid[id]-l[id]+1)*h[mid[id]]);
// printf("id=%d qr=%d\n",id,qr);
}
// if(154<=nl&&nr<=158) for(int i=nl;i<=nr;i++) printf("%lld ",get(1,1,n,i));
// printf("\n\n");
}
void run()
{
for(int i=1;i<=m;i++) if(mid[i]!=r[i])
q[get_max(mid[i]+1,r[i])].push_back(pi(i,r[i]));
dfs(1,n);
}
void reverse()
{
flag=1;
// for(int i=1;i<=m;i++) printf("%lld\n",ans[i]);
cover(1,1,n,1,n,0),reverse(h+1,h+n+1),run_st();
for(int i=1;i<=m;i++)
{
l[i]=n-l[i]+1,r[i]=n-r[i]+1;
swap(l[i],r[i]),mid[i]=get_max(l[i],r[i]);
}
for(int i=1;i<=n;i++) q[i].clear();
}
int main()
{
n=read(),m=read();
for(int i=1;i<=n;i++) h[i]=read(),st[i][0]=i;
for(int i=0;(1<<i)<=n;i++) p[1<<i]=i;
for(int i=1;i<=n;i++) if(!p[i]) p[i]=p[i-1];
run_st();
for(int i=1;i<=m;i++)
{
l[i]=read()+1,r[i]=read()+1;
mid[i]=get_max(l[i],r[i]);
ans[i]=(ll)(r[i]-l[i]+1)*h[mid[i]];
}
run(),reverse(),run();
for(int i=1;i<=m;i++) printf("%lld\n",ans[i]);
return 0;
}
/*
13 1
2 3 2 4 5 1 2 4 4 3 3 3 3
5 7
*/

我来说一下我都翻过什么智障错误:

  1. 线段树查询的时候不加 push_down

  2. st 表从前往后处理。

  3. 线段树上二分没有判区间范围。

甚至我在调第三个错误的时候对着一组 的数据看了半天。

注意:还有一个小细节,就是翻转数组之后,笛卡尔树一定要严格镜像翻转。

也就是说假如一个区间有多个最大值,第一次你选了最左边的最大值当作区间的根,那么翻转之后也一定要让原来这个元素当根,也就是现在区间最右边的最大值。