复盘
7:40 成功开题,艾教场
看 T1,感觉很神奇,但看数据范围没有开到 1 e 9 1e9 1e9 之类的,应该是得怎么搜一下;T2 发现做过原题,秒了;T3 第一眼没看懂题,推样例始终不理解其中的一句话;T4 听说是 arc 原,看起来应该可做
尝试 T1,爆搜给了 45pts,想了想可以把一段操作打包做,不以操作为阶段,而以每次往桶里倒水为阶段,这样可以在 O ( x y z ) O(xyz) O(xyz) 的复杂度内把所有边搜出来,最后再跑最短路
感觉很对,遂开写,20min 写的暴力跑出来了大样例,准备优化 (噩梦开始)
首先想到的操作是 ( z − y − . . . ) , ( z − x − . . . ) , ( y − x − . . . ) (z-y-...),(z-x-...),(y-x-...) (z−y−...),(z−x−...),(y−x−...),然后这会有个问题,比如说先灌满 z z z,然后不停往 x x x 里倒,最后得到 z − k x z-kx z−kx 的一个东西,再倒进桶里。问题在于 最后一次 z z z 可以不清空 ,就是说可能留着 z z z 为满的情况下只使用 x , y x,y x,y 把桶灌到合适为止?那这样图得分层, f [ i ] [ 0 / 1 / 2 / 3 / 4 ] f[i][0/1/2/3/4] f[i][0/1/2/3/4] 表示能用 x y z / x y / x z / y z / z xyz/xy/xz/yz/z xyz/xy/xz/yz/z 的情况下的最小值
写写写,一顿分讨后代码长度来到 250 行,已经 1h 过去,但心想 T2 已经 100 了,不慌
发现样例没过,推了一下感觉样例错了???但爆搜跑出来的没问题啊???直接懵逼了
静态查错了大概 20min,未果…已经 10:15
决定先把 T2 写了,Kruskal 重构树+经典结论维护 dfn 最小最大的 LCA,十分顺利,样例一遍过,10:40 交了,发现这题没卡 l o g 2 log^2 log2 做法,可惜
回看 T1,仍觉得样例不对
看 T3,发现题根本看不懂?不懂样例解释里说 “1、4不能” 通信是什么意思,明明可以选 4 4 4 啊?无奈只能跳过
先看 T4 吧,想 q n qn qn 做法瞪出一个性质:必定选一段长度为 1 1 1,很快把暴力写完了,只剩 40min 了!
再看 T1 ,发现那个样例是因为我看错了,滑动条拉到最右边导致我看的是后面几个答案…
难蚌,经过手玩已经找到之前的问题了,不仅可以 z − x − x − . . . z-x-x-... z−x−x−... 还可以 x + . . + x − z x+..+x-z x+..+x−z 得到新的,这样讨论完全是 2 3 = 8 2^3=8 23=8 种状态!
决定把之前的删掉重构一遍,在过程中算复杂度发现 8 ∗ 1 e 5 ∗ 300 ∗ l o g 8*1e5*300*log 8∗1e5∗300∗log…心态有点崩
最后也是不出意料地没写完
35+100+0+40=175 , rk_ O ( n ! ) O(n!) O(n!)
懂了,艾教场一定要放弃 T1
T1 离正解很近了… 考虑把上面那种难处理地操作直接换到最后做,就可以直接背包了。推出来离谱做法后应该再尝试优化一下的…
T3 70pts 的主席树几乎是送的…(甚至实现好可以拿100),而我样例都没推懂…
结论:要学会放弃艾教的 T1
题解
T1
我们理想的状态当然是 每次往桶里倒水的操作互不影响,这样就可以
O
(
x
y
z
)
O(xyz)
O(xyz) 处理后直接背包
可以证明,一定存在一种最优方案,会影响后面的操作只会在最后做一次
有影响指的是 “某个杯子不清空,在之后的操作中不再用到” ,不妨设为是 x x x 杯,由于操作顺序不固定,那么之后 y , z y,z y,z 的清空操作不妨直接放到前面完成
唯一的问题是如果在接下来又在 y , z y,z y,z 中执行了一次 “对后面有影响” 的操作呢?手玩后发现,这样的情况一定可以被另一种上文所说的方式代替掉!
那么做法就很简单:处理 b t [ x ] bt[x] bt[x] 表示往桶里倒 x x x 的水,且清空,最小操作次数; b t 2 [ x ] bt2[x] bt2[x] 表示不清空,最小次数
对于前者做完全背包,对于后者做分组背包
#include<bits/stdc++.h>
using namespace std ;
typedef long long LL ;
const int N = 1e5+10 ;
int X , Y , Z , n ;
namespace Part1
{
// 操作顺序可以随便交换,不妨就把填满某个不再动的操作放到最后
struct nn
{
int x , y , z , d ;
};
int D[101][101][101] , f[N] , bt[301] , bt2[301] ;// 最后一次操作
queue<nn> q ;
void solve()
{
memset( D , -1 , sizeof D ) ;
memset( bt , 0x3f , sizeof bt ) ;
memset( bt2 , 0x3f , sizeof bt2 ) ;
q.push({0,0,0,0}) ;
D[0][0][0] = 0 ;
while( !q.empty() ) {
int x=q.front().x,y=q.front().y,z=q.front().z,d=q.front().d;
q.pop() ;
bt[x+y+z] = min( bt[x+y+z] , d+(x!=0)+(y!=0)+(z!=0) ) ;
bt2[x] = min(bt2[x],d+(x!=0)); bt2[y] = min(bt2[y],d+(y!=0)); bt2[z] = min(bt2[z],d+(z!=0)) ;
bt2[x+y]=min(bt2[x+y],d+(x!=0)+(y!=0)),bt2[x+z]=min(bt2[x+z],d+(x!=0)+(z!=0)),bt2[y+z]=min(bt2[y+z],d+(y!=0)+(z!=0)) ;
if( D[X][y][z] == -1 ) D[X][y][z] = d+1 , q.push({X,y,z,d+1}) ;
if( D[x][Y][z] == -1 ) D[x][Y][z] = d+1 , q.push({x,Y,z,d+1}) ;
if( D[x][y][Z] == -1 ) D[x][y][Z] = d+1 , q.push({x,y,Z,d+1}) ;
if( D[0][y][z] == -1 ) D[0][y][z] = d+1 , q.push({0,y,z,d+1}) ;
if( D[x][0][z] == -1 ) D[x][0][z] = d+1 , q.push({x,0,z,d+1}) ;
if( D[x][y][0] == -1 ) D[x][y][0] = d+1 , q.push({x,y,0,d+1}) ;
//x->y
int tim=min(x,Y-y); if( D[x-tim][y+tim][z]==-1 ) D[x-tim][y+tim][z]=d+1 , q.push({x-tim,y+tim,z,d+1}) ;
tim = min(x,Z-z); if( D[x-tim][y][z+tim]==-1 ) D[x-tim][y][z+tim]=d+1 , q.push({x-tim,y,z+tim,d+1}) ;
tim = min(y,X-x); if( D[x+tim][y-tim][z]==-1 ) D[x+tim][y-tim][z]=d+1 , q.push({x+tim,y-tim,z,d+1}) ;
tim = min(y,Z-z); if( D[x][y-tim][z+tim]==-1 ) D[x][y-tim][z+tim]=d+1 , q.push({x,y-tim,z+tim,d+1}) ;
tim = min(z,X-x); if( D[x+tim][y][z-tim]==-1 ) D[x+tim][y][z-tim]=d+1 , q.push({x+tim,y,z-tim,d+1}) ;
tim = min(z,Y-y); if( D[x][y+tim][z-tim]==-1 ) D[x][y+tim][z-tim]=d+1 , q.push({x,y+tim,z-tim,d+1}) ;
}
memset( f , 0x3f , sizeof f ) ;
f[0] = 0 ;
for(int i = 1 ; i <= X+Y+Z ; i ++ ) {
if( bt[i]>1000 ) continue ;
for(int j = i ; j <= n ; j ++ ) { // 完全背包
f[j] = min( f[j] , f[j-i]+bt[i] ) ;
}
}
for(int j = n ; j >= 1 ; j -- ) {
for(int i = 1 ; i <= min(j,X+Y+Z) ; i ++ ) {
if( bt2[i] > 1000 ) continue ;
f[j] = min( f[j] , f[j-i]+bt2[i] ) ;
}
}
for(int i = 1 ; i <= n ; i ++ ) printf("%d " , f[i]>1e8?-1:f[i] ) ;
}
}
int main()
{
scanf("%d%d%d%d" , &X , &Y , &Z , &n ) ;
if( X > Y ) swap( X , Y ) ;
if( Y > Z ) swap( Y , Z ) ;
if( X > Y ) swap( X , Y ) ;
Part1::solve() ;
return 0 ;
}
T2
典题
T3
考虑确定
P
P
P 后,每个节点
z
z
z 的负载是多少,等价于查以它为根的各个子树中的
s
i
z
siz
siz
那么二分答案后 check ,check 方法很多,已知的:线段树合并/主席树 在原序列上做;按 w i w_i wi 排序后离线树状数组做
但这样不可避免二分答案的 l o g log log + 检验的 l o g log log
我们之前遇到这个问题是在 区间第 K 大,当时利用了线段树二分来解决,我们考虑同样编一个线段树二分的做法
首先对于每个节点还是要维护子树中 w i w_i wi 的值域,通过 d f n dfn dfn 序转化到序列上 前缀主席树实现
接下来直接在查询的过程中二分,取出若干个主席树的根节点,每次计算左子树的答案,判断后 整体 往左/右儿子走
口胡很简单,细节不少
#include<bits/stdc++.h>
using namespace std ;
typedef long long LL ;
const int N = 3e5+10 , INF = 1e6 ;
int n , w[N] ;
LL K ;
vector<int> E[N] ;
int dfn[N] , tim , ed[N] , nam[N] ;
void dfs( int x , int fa )
{
dfn[x] = ++tim ; nam[tim] = x ;
for(int t : E[x] ) {
if( t == fa ) continue ;
dfs( t , x ) ;
}
ed[x] = tim ;
}
struct Segtree
{
int ls , rs , sum ;
}t[32*N] ;
int tot , rt[N] ;
inline int build() { return ++tot ; }
int Insert( int p , int l , int r , int x )
{
int nw = build() ;
t[nw] = t[p] ;
if( l == r ) {
t[nw].sum ++ ;
return nw ;
}
int mid = (l+r)>>1 ;
if( x <= mid ) t[nw].ls = Insert( t[p].ls , l , mid , x ) ;
else t[nw].rs = Insert( t[p].rs , mid+1 , r , x ) ;
t[nw].sum = t[t[nw].ls].sum + t[t[nw].rs].sum ;
return nw ;
}
int p[N] , q[N] , Sum[N] , len , X , S , R ;//q-p
LL calc( int mid )
{
int siz = 0 ; LL sum = 0 ;
if( w[X] <= mid ) siz = 1 ;
for(int i = 1 ; i <= len ; i ++ ) {
int v = t[q[i]].sum - t[p[i]].sum + Sum[i] ;
sum += 1LL*siz*v ;
siz += v ;
}
int v = t[R].sum-siz+S ;
sum += 1LL*siz*v ;
siz += v ;
return sum ;
}
int ask( int l , int r )
{
if( l == r ) {
if( calc(l) < K ) return l ;// 需要再算一次,不然边界会错
return l-1 ;
}
int mid = (l+r)>>1 ;
// 选左子树答案
int siz = 0 ; LL sum = 0 ;
if( w[X] <= mid ) siz = 1 ;
for(int i = 1 ; i <= len ; i ++ ) {
int v = t[t[q[i]].ls].sum - t[t[p[i]].ls].sum + Sum[i] ;
sum += 1LL*siz*v ;
siz += v ;
}
int v = t[t[R].ls].sum-siz+S ;
sum += 1LL*siz*v ;
siz += v ;
if( sum < K ) { //可以更大,往右儿子走
for(int i = 1 ; i <= len ; i ++ ) {
Sum[i] += t[t[q[i]].ls].sum-t[t[p[i]].ls].sum ;// 需要补前缀
q[i] = t[q[i]].rs ; p[i] = t[p[i]].rs ;
}
S += t[t[R].ls].sum ;
R = t[R].rs ;
return ask( mid+1 , r ) ;
}
else { // 往左儿子走
for(int i = 1 ; i <= len ; i ++ ) {
q[i] = t[q[i]].ls ; p[i] = t[p[i]].ls ;
}
R = t[R].ls ;
return ask( l , mid ) ;
}
}
void solve()//把二分放到主席树上
{
int Min = 1e6 ;
for(int x = 1 ; x <= n ; x ++ ) {
X = x ; S = 0 ; R = rt[n] ; len = 0 ;
for(int t : E[x] ) {
if( dfn[t] < dfn[x] ) continue ;
len ++ ;
p[len] = rt[dfn[t]-1] ; q[len] = rt[ed[t]] ; Sum[len] = 0 ;//这一层的根全存起来,整体走
}
Min = min( Min , ask(0,INF)-w[x] ) ;
}
printf("%d\n" , Min ) ;
}
int main()
{
scanf("%d%lld" , &n , &K ) ;
for(int i = 1 ; i <= n ; i ++ ) {
scanf("%d" , &w[i] ) ;
}
int x , y ;
for(int i = 1 ; i < n ; i ++ ) {
scanf("%d%d" , &x , &y ) ;
E[x].push_back( y ) ;
E[y].push_back( x ) ;
}
dfs( 1 , 0 ) ; // 需要子树信息,除了线段树合并之外,还可以通过 dfn 搞成前缀主席树
rt[0] = build() ;
for(int i = 1 ; i <= n ; i ++ ) {
rt[i] = Insert( rt[i-1] , 0 , INF , w[nam[i]] ) ;
}
solve() ;
return 0 ;
}
类似于 树套树求区间第
K
K
K 大时,整体往下走的思路
T4
好题,不过似乎比较套路
枚举 其中一段后 后,只有两种情况
这是由于,后边的两段中 有一段内部最大值更大,把这一段尽可能延申一定不差
( bb几句:好吧赛场上没有继续推下去,所以遇到这种情况我们要最大限度利用贪心简化条件
那么同理对于左边的两段,我们在比较二者内部的最大值后也可以进行延申
总之,最后只会存在两种情况:
[ L , L ] , [ L + 1 , R − 1 ] , [ R , R ] [L,L],[L+1,R-1],[R,R] [L,L],[L+1,R−1],[R,R]
[ L , t − 1 ] , [ t , t ] , [ t + 1 , R ] [L,t-1],[t,t],[t+1,R] [L,t−1],[t,t],[t+1,R]
第一种直接算就行,对于第二种情况:
首先它有三个变量,不好搞
对于区间 M a x Max Max 可以有一种想法:由于一定有一段能够取到整个区间的 M a x Max Max, 我们考虑它的位置,通过 下标在某一区间内 把某个变量给定下来
具体来说,假设区间最大值的位置为 p o s pos pos:(有多个取最左边)
不妨认为 t < p o s t<pos t<pos,那么第三段的答案就是固定的 M a x Max Max,接下来只需要求 m a x ( a L , . . . , a t − 1 ) + a t max(a_L,...,a_{t-1})+a_t max(aL,...,at−1)+at 的最大值
与下标有关,我们按 L L L 扫描线,线段树下标 t t t 位置维护的就是 t t t 的答案
当 L − − L-- L−−,对线段树的影响是修改了某一段的区间最大值,通过 单调栈找到影响的范围 + 线段树区间修改 解决;然后新插入 L L L 位置的答案也是好做的
对于一个询问,只需查 [ L + 1 , p o s − 1 ] [L+1,pos-1] [L+1,pos−1] 内部的答案即可, p o s pos pos 是区间最大值位置
对于 t > p o s t>pos t>pos 只需要把序列翻转后做一遍即可
#include<bits/stdc++.h>
using namespace std ;
typedef long long LL ;
const int N = 3e5+100 ;
int n , Q , a[N] , st[20][N] , id[20][N] , Lg2[N] ;
void make_st()
{
for(int i = 2 ; i <= n ; i ++ ) Lg2[i] = Lg2[i/2]+1 ;
for(int i = 0 ; i <= 18 ; i ++ ) {
if( i == 0 ) {
for(int j = 1 ; j <= n ; j ++ ) st[i][j] = a[j] , id[i][j] = j ;
}
else {
for(int j = 1 ; j <= n-(1<<i)+1 ; j ++ ) {
if( st[i-1][j] >= st[i-1][j+(1<<(i-1))] ) { // 多个 Max,取最靠左
st[i][j] = st[i-1][j] ;
id[i][j] = id[i-1][j] ;
}
else {
st[i][j] = st[i-1][j+(1<<(i-1))] ;
id[i][j] = id[i-1][j+(1<<(i-1))] ;
}
}
}
}
}
typedef pair<int,int> PII ;
PII ask( int l , int r )
{
int k = Lg2[r-l+1] ;
if( st[k][l] >= st[k][r-(1<<k)+1] ) {
return {st[k][l],id[k][l]} ;
}
return {st[k][r-(1<<k)+1],id[k][r-(1<<k)+1]} ;
}
// 推性质,简化条件
// 对于 Max,很常见的处理是考虑最大值位置,转化成某一段区间内某个值是一定的,减少变量
struct nn
{
int l , r , id , Mx , pos ;
}q[N] ;
bool cmp( nn x , nn y )
{
return x.l > y.l ;
}
int ans[N] , stk[N] , top ;
struct Segtree
{
int l , r , Max , Val , Mx ;//单点最小值/区间最大值/和的最小值
int tag ;
}t[4*N] ;
inline void update( int p )
{
t[p].Mx = min( t[p<<1].Mx , t[p<<1|1].Mx ) ;
t[p].Max = min( t[p<<1].Max , t[p<<1|1].Max ) ;
}
void build( int p , int l , int r )
{
t[p].l = l , t[p].r = r , t[p].Max = t[p].Val = t[p].Mx = t[p].tag = 0 ;
if( l == r ) {
t[p].Max = a[l] ;
t[p].Mx = a[l] ;
return ;
}
int mid = (t[p].l+t[p].r)>>1 ;
build( p<<1 , l , mid ) ; build( p<<1|1 , mid+1 , r ) ;
update( p ) ;
}
inline void spread( int p )
{
if( t[p].tag ) { // 强行让后来的覆盖前面的
int d = t[p].tag ; t[p].tag = 0 ;
t[p<<1].Val = d ; t[p<<1].tag = d ; t[p<<1].Mx = t[p<<1].Max+t[p<<1].Val ;
t[p<<1|1].Val = d ; t[p<<1|1].tag = d ; t[p<<1|1].Mx = t[p<<1|1].Max+t[p<<1|1].Val ;
}
}
void change( int p , int l , int r , int x ) //[l,r]内的区间最大值更改为 x
{
if( l <= t[p].l && t[p].r <= r ) {
t[p].Val = x ; t[p].tag = x ;
t[p].Mx = t[p].Max+t[p].Val ;
return ;
}
spread(p) ;
int mid = (t[p].l+t[p].r)>>1 ;
if( l <= mid ) change( p<<1 , l , r , x ) ;
if( r > mid ) change( p<<1|1 , l , r , x ) ;
update( p ) ;
}
int query( int p , int l , int r ) // [l,r] 内和的最大值
{
if( l <= t[p].l && t[p].r <= r ) {
return t[p].Mx ;
}
spread(p) ;
int mid = (t[p].l+t[p].r)>>1 , res = 1e9 ;
if( l <= mid ) res = min( res , query(p<<1,l,r) ) ;
if( r > mid ) res = min( res , query(p<<1|1,l,r) ) ;
return res ;
}
void solve()
{
sort( q+1 , q+Q+1 , cmp ) ;
build( 1 , 1 , n ) ;
stk[0] = n ;
for(int i = n , j = 1 ; i >= 1 ; i -- ) {
while( top && a[i]>a[stk[top]] ) top -- ;
int R = stk[top] ;
stk[++top] = i ;
if( i+1 <= R ) {
change( 1 , i+1 , R , a[i] ) ;
}
while( j <= Q && q[j].l == i ) {
if( i+1<=q[j].pos-1 ) {
ans[q[j].id]=min(ans[q[j].id],query(1,i+1,q[j].pos-1)+q[j].Mx ) ;
}
j ++ ;
}
}
}
int main()
{
scanf("%d%d" , &n , &Q ) ;
for(int i = 1 ; i <= n ; i ++ ) {
scanf("%d" , &a[i] ) ;
}
make_st() ;
for(int i = 1 ; i <= Q ; i ++ ) {
scanf("%d%d" , &q[i].l , &q[i].r ) ;
q[i].id = i ;
PII x = ask(q[i].l,q[i].r) ;
q[i].Mx = x.first , q[i].pos = x.second ;
ans[i] = a[q[i].l]+a[q[i].r]+ask(q[i].l+1,q[i].r-1).first ;
}
solve() ;
reverse( a+1 , a+n+1 ) ;
for(int i = 1 ; i <= Q ; i ++ ) {
q[i].l = n-q[i].l+1 , q[i].r = n-q[i].r+1 ;
swap( q[i].l , q[i].r ) ;
q[i].pos = n-q[i].pos+1 ;
}
solve() ;
for(int i = 1 ; i <= Q ; i ++ ) {
printf("%d\n" , ans[i] ) ;
}
return 0 ;
}