2024.11.19

First Post:

Last Update:

Word Count:
3.4k

Read Time:
18 min

怎么什么套路也不会?怎么什么套路也不会?怎么什么套路也不会?

一类DP

总结转移里带最值的一类 DP。

对于转移为 ,如果用 ST 表预处理,然后暴力转移的话,复杂度是 的。

然后有一个套路,出自 Pudding Monsters

,可以发现,序列 一定是形如下图的一些连续段:

根据 的单调性,连续段的权值一定单调递减。

所以我们可以维护一个单调递减的单调栈,在加入元素 时,维护单调栈并更新一些段的 值。

然后可以用线段树维护这个东西。

具体的,我们枚举 ,然后用单调栈维护 ,对应的修改在线段树上完成。

复杂度

换成 同理,不再赘述。

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
#include"bits/stdc++.h"
#define re register
#define int long long
#define ls(p) tr[p].ls
#define rs(p) tr[p].rs
using namespace std;
const int maxn=1e5+10,inf=1e18;
int n,top,rt,segcnt;
int a[maxn],stk[maxn],f[maxn];
struct tree{
int ls,rs,mx,tag;
}tr[maxn<<1];
inline void up(int p){tr[p].mx=max(tr[ls(p)].mx,tr[rs(p)].mx);}
inline void add(int p,int val){tr[p].mx+=val,tr[p].tag+=val;}
inline void down(int p){if(tr[p].tag) add(ls(p),tr[p].tag),add(rs(p),tr[p].tag),tr[p].tag=0;}
inline void build(int l,int r,int &p){
p=++segcnt;
if(l==r) return;
int mid=(l+r)>>1;
build(l,mid,ls(p)),build(mid+1,r,rs(p));
}
inline void update(int l,int r,int L,int R,int val,int p){
if(l>=L&&r<=R){add(p,val);return;}
int mid=(l+r)>>1;down(p);
if(L<=mid) update(l,mid,L,R,val,ls(p));
if(R>mid) update(mid+1,r,L,R,val,rs(p));
up(p);
}
inline int query(int l,int r,int L,int R,int p){
if(l>=L&&r<=R) return tr[p].mx;
int mid=(l+r)>>1,res=-inf;down(p);
if(L<=mid) res=max(res,query(l,mid,L,R,ls(p)));
if(R>mid) res=max(res,query(mid+1,r,L,R,rs(p)));
return res;
}
signed main(){
#ifndef ONLINE_JUDGE
freopen("1.in","r",stdin);
freopen("1.out","w",stdout);
#endif
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n;build(0,n,rt);
for(re int i=1;i<=n;++i) cin>>a[i];
for(re int i=1;i<=n;++i){
while(top&&a[stk[top]]<=a[i]) update(0,n,stk[top-1],stk[top]-1,-a[stk[top]],rt),--top;
update(0,n,stk[top],i-1,a[i],rt),stk[++top]=i;
f[i]=query(0,n,0,i-1,rt);
update(0,n,i,i,f[i],rt);
}
cout<<f[n];
return 0;
}

然后来看下面这道题。

刚才已经维护好 了,考虑怎么维护

可以发现, 也是有单调性的,也会形成一些 连续段。

考虑加入元素 带来的影响。

可以发现,只有 的那一段会受到影响,且这一段会分裂成若干不同段。

举个例子:

图中的 表示 ,且还没有考虑到 的影响。

下面我们就来考虑 会对前面的 连续段有什么影响。

可以发现,加入 后,连续段会变成这样:

我们可以维护 表示 值为 的连续段的左右端点。

我们从右端点开始向前推:先找到右端点新的 ,然后向前找到这个值在的位置,把这个值所在位置的后一个到右端点这一部分的 修改(因为这一段的 都是这个值),然后更新右端点。

以上图为例,我们先找到 的新 ,发现是 ,然后找到 的位置,把这一段的 都更新成 ,然后更新右端点,继续操作。

关于找到新的 是几以及它的位置,可以维护一颗权值线段树,然后在权值线段树上二分。

修改次数均摊是 的,只能感性理解了。

具体可以看这道题:Rmq Problem / mex

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
#include"bits/stdc++.h"
#define re register
#define int long long
#define ls(p) tr[p].ls
#define rs(p) tr[p].rs
#define pii pair<int,int>
#define fi first
#define se second
using namespace std;
const int maxn=2e5+10,inf=1e18;
int n,A,B,C,D;
int a[maxn],f[maxn];
int L[maxn],R[maxn];
struct Stack{
int top;
int stk[maxn];
}mn,mx;
namespace tree1{
int rt,segcnt;
struct tree{
int ls,rs,mx,tag;
}tr[maxn<<1];
inline void up(int p){tr[p].mx=max(tr[ls(p)].mx,tr[rs(p)].mx);}
inline void add(int p,int val){tr[p].mx+=val,tr[p].tag+=val;}
inline void down(int p){if(tr[p].tag) add(ls(p),tr[p].tag),add(rs(p),tr[p].tag),tr[p].tag=0;}
inline void build(int l,int r,int &p){
p=++segcnt;tr[p].mx=tr[p].tag=0;
if(l==r) return;
int mid=(l+r)>>1;
build(l,mid,ls(p)),build(mid+1,r,rs(p));
}
inline void update(int l,int r,int L,int R,int val,int p){
if(l>=L&&r<=R){add(p,val);return;}
int mid=(l+r)>>1;down(p);
if(L<=mid) update(l,mid,L,R,val,ls(p));
if(R>mid) update(mid+1,r,L,R,val,rs(p));
up(p);
}
inline int query(int l,int r,int L,int R,int p){
if(l>=L&&r<=R) return tr[p].mx;
int mid=(l+r)>>1,res=-inf;down(p);
if(L<=mid) res=max(res,query(l,mid,L,R,ls(p)));
if(R>mid) res=max(res,query(mid+1,r,L,R,rs(p)));
return res;
}
inline void clear(){rt=segcnt=0;}
}
namespace tree2{
int rt,segcnt;
struct tree{
int ls,rs;
pii s;
}tr[maxn<<1];
inline void up(int p){tr[p].s=min(tr[ls(p)].s,tr[rs(p)].s);}
inline void build(int l,int r,int &p){
p=++segcnt;
if(l==r){tr[p].s={0,l};return;}
int mid=(l+r)>>1;
build(l,mid,ls(p)),build(mid+1,r,rs(p));
up(p);
}
inline void update(int l,int r,int pos,int val,int p){
if(l==r){tr[p].s={val,pos};return;}
int mid=(l+r)>>1;
if(pos<=mid) update(l,mid,pos,val,ls(p));
else update(mid+1,r,pos,val,rs(p));
up(p);
}
inline pii query(int l,int r,int pos,int p){
if(tr[p].s.fi>=pos) return {-1,-1};
if(l==r) return tr[p].s;
int mid=(l+r)>>1;
if(tr[ls(p)].s.fi>=pos) return query(mid+1,r,pos,rs(p));
else return query(l,mid,pos,ls(p));
}
inline void clear(){rt=segcnt=0;}
}
inline void clear(){
for(re int i=0;i<=n;++i) L[i]=R[i]=-1;L[0]=1,R[0]=n;
for(re int i=1;i<=n;++i) f[i]=0;
mn.top=mx.top=0;
tree1::clear();tree2::clear();
}
inline void solve(){
cin>>n>>A>>B>>C>>D;clear();tree1::build(0,n,tree1::rt);tree2::build(0,n,tree2::rt);
for(re int i=1;i<=n;++i) cin>>a[i];
for(re int i=1,x;i<=n;++i){
while(mx.top&&a[mx.stk[mx.top]]<=a[i]) tree1::update(0,n,mx.stk[mx.top-1],mx.stk[mx.top]-1,-A*a[mx.stk[mx.top]],tree1::rt),--mx.top;
tree1::update(0,n,mx.stk[mx.top],i-1,A*a[i],tree1::rt),mx.stk[++mx.top]=i;
while(mn.top&&a[mn.stk[mn.top]]>=a[i]) tree1::update(0,n,mn.stk[mn.top-1],mn.stk[mn.top]-1,-B*a[mn.stk[mn.top]],tree1::rt),--mn.top;
tree1::update(0,n,mn.stk[mn.top],i-1,B*a[i],tree1::rt),mn.stk[++mn.top]=i;
x=a[i];
tree2::update(0,n,x,i,tree2::rt);
if(L[x]!=-1){
int l=L[x],r=min(R[x],i);
if(x!=0) L[x]=R[x]=-1;
else L[x]=i+1;
tree1::update(0,n,l-1,r-1,-C*x,tree1::rt);
while(l<=r){
pii res=tree2::query(0,n,r,tree2::rt);
int pos=res.fi,val=res.se;++pos;
if(l<=pos){
L[val]=pos,R[val]=r;
tree1::update(0,n,pos-1,r-1,C*val,tree1::rt);
}
else{
R[val]=r;
tree1::update(0,n,l-1,r-1,C*val,tree1::rt);
}
r=pos-1;
}
}
f[i]=tree1::query(0,n,0,i-1,tree1::rt)+D;
tree1::update(0,n,i,i,f[i],tree1::rt);
}
cout<<f[n]<<'\n';
}
signed main(){
freopen("mix.in","r",stdin);
freopen("mix.out","w",stdout);
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int T;cin>>T;while(T--) solve();
return 0;
}

一类二分

Minimax Problem

看到一堆 套在一起,想到用二分去掉一个限制。

设当前二分到 ,然后把 的位置设为 ,其他位置设为

然后把一行状压成一个数,那么当前答案合法当且仅当存在两个数或起来是全集。

判断就暴力 即可。

复杂度

感觉类似套路的题有一车,就是二分答案,然后把大于答案的看成 ,小于的看成 或者

最短路树/最短路 DAG

没咋做过这种题,但是最近见到了一些。

感觉这个概念好像也不太重要?

[COCI2008-2009#3] NAJKRACI

见到这道题后,想到的第一个暴力做法可能是:枚举边和点对,然后看这条边是否在两点的最短路上。

但两点间的最短路可能有多条,暴力判断也不太好判断。

我们考虑从点 跑最短路,得到一张最短路 DAG。

对于一条在该 DAG 上的边 来说,它的贡献为

其中 表示 的最短路数量, 表示 后面的点的数量。

具体实现可以先跑最短路,然后拓扑排序求 ,不过我写了 ,本质相同。

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
#include"bits/stdc++.h"
#define re register
#define int long long
#define pii pair<int,int>
#define fi first
#define se second
using namespace std;
const int maxn=1510,maxm=5e3+10,mod=1e9+7,inf=1e18;
int n,m,tot;
int head[maxn],dis[maxn],cnt[maxn],sum[maxn];
int ans[maxm];
bitset<maxn> vis;
struct edge{
int to,nxt,w,id;
}e[maxm];
inline void add(int u,int v,int w){
++tot,e[tot]={v,head[u],w,tot};
head[u]=tot;
}
inline void dij(int s){
for(re int i=1;i<=n;++i) dis[i]=inf,vis[i]=0,cnt[i]=0;
priority_queue<pii,vector<pii>,greater<pii>> q;
q.push({0,s});dis[s]=0,cnt[s]=1;
while(!q.empty()){
int u=q.top().se;q.pop();
if(vis[u]) continue;vis[u]=1;
for(re int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(dis[v]>dis[u]+e[i].w){
dis[v]=dis[u]+e[i].w;
cnt[v]=cnt[u];
q.push({dis[v],v});
}
else if(dis[v]==dis[u]+e[i].w) cnt[v]+=cnt[u];
}
}
}
inline void clear(){for(re int i=1;i<=n;++i) sum[i]=0;}
inline void dfs(int u){
if(sum[u]) return;
for(re int i=head[u];i;i=e[i].nxt){
int v=e[i].to;
if(dis[v]==dis[u]+e[i].w){
dfs(v);
ans[e[i].id]=(ans[e[i].id]+cnt[u]*sum[v])%mod;
sum[u]+=sum[v];
}
}
++sum[u];
}
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>m;
for(re int i=1,u,v,w;i<=m;++i) cin>>u>>v>>w,add(u,v,w);
for(re int i=1;i<=n;++i) dij(i),clear(),dfs(i);
for(re int i=1;i<=m;++i) cout<<ans[i]<<'\n';
return 0;
}

模拟赛

数据范围:

核心思路还是拆贡献。

设加进来的边为 ,考虑这条边带来的影响。

我们固定一个源点 ,设边 会对 产生影响,则

移项,得到

可以发现,这条边会使花费减少

考虑如何求出每条边带来的贡献。

可以发现,对于一对 ,合法的 是一段后缀。

所以可以枚举 ,然后对每个点 维护后缀和,同时还要维护个数。

然后枚举 算贡献即可。

那么最后的答案就是选使花费减少的最多的那些边。

复杂度

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
#include"bits/stdc++.h"
#define re register
#define int long long
using namespace std;
const int maxn=610;
int n,m,ans;
int dis[maxn][maxn];
bool vis[maxn][maxn];
int sum[maxn][maxn],cnt[maxn][maxn];
int c[maxn][maxn];
inline void Floyd(){
for(re int i=1;i<=n;++i) dis[i][i]=0;
for(re int k=1;k<=n;++k){
for(re int i=1;i<=n;++i){
for(re int j=1;j<=n;++j){
dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
}
}
}
}
signed main(){
#ifndef ONLINE_JUDGE
freopen("1.in","r",stdin);
freopen("1.out","w",stdout);
#endif
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>m;memset(dis,0x3f,sizeof dis);
for(re int i=1,u,v;i<=m;++i){
cin>>u>>v;
dis[u][v]=dis[v][u]=1;
vis[u][v]=vis[v][u]=1;
}
Floyd();
for(re int i=1;i<=n;++i) for(re int j=i+1;j<=n;++j) ans+=dis[i][j];
for(re int s=1;s<=n;++s){
for(re int v=1;v<=n;++v){
for(re int j=0;j<=n;++j) sum[v][j]=cnt[v][j]=0;
for(re int t=1;t<=n;++t){
int val=dis[s][t]-dis[v][t]-1;
if(val<0) continue;
sum[v][val]+=val;
++cnt[v][val];
}
for(re int j=n-1;j;--j){
sum[v][j]+=sum[v][j+1];
cnt[v][j]+=cnt[v][j+1];
}
}
for(re int u=1;u<=n;++u){
for(re int v=u+1;v<=n;++v){
if(vis[u][v]) continue;
int D=dis[s][u]+1;
c[u][v]+=sum[v][D]-cnt[v][D]*dis[s][u];
}
}
}
int res=0;
for(re int u=1;u<=n;++u){
for(re int v=u+1;v<=n;++v){
if(vis[u][v]) continue;
res=max(res,c[u][v]);
}
}
int num=0;
for(re int u=1;u<=n;++u){
for(re int v=u+1;v<=n;++v){
if(vis[u][v]) continue;
if(c[u][v]==res) ++num;
}
}
cout<<ans-res<<' '<<num;
return 0;
}