NRE 枚举dp+线段树加速

题目:https://vjudge.net/contest/433343#problem/A

题意:给定01序列b,和一个全为0的等长的序列a。有Q种操作,每个操作有l和r,可以让a中下标l到r的0变成1。每个操作可用可不用。问操作完(可以甚至一个操作都不做)后ab对应下标数据不同的一共有多少组。

参考博客:https://blog.csdn.net/wangyiyang2/article/details/104758559?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-2.control&dist_request_id=1331645.9479.16184005117579903&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-2.control

思路:将题目转化为求对应下标下a数组中值为0b中为1的个数+a中为1b中为0的个数。(为了方便用num(a0b1+a1b0)来表示)。其中,b为0的个数肯定是一个常数b0(因为b数组已知确定)。而a0b0+a1b0=b0。

因此原题意可以转化为求num(a0b1+b0-a0b0)。因为b0为常数 所以继续转化为求

num(a0b1-a0b0)。

接下来考虑状态转移。对于每一个操作只有两种状态,取或者不取。dp[i]表示到下标i为止num(a0b1-a0b0)的最小值。

 

对于下标i开始的一个操作,我们先考虑不取的情况。

由于在下标i不取操作,那么ai==0,因此如果bi=0 新增的num就为0-1=-1;如果bi=1 新增的num就为1-0=1;

dp[i]=min(dp[i], dp[i-1]+(b[i]==1)?1:-1;

此情况结束。

 

我们再来考虑在i上取某个操作的情况。用l和r表示对应操作的左右下标。(也就是i=l)

先说结论,dp[b]=min(dp[r-1],dp[r-2]...dp[l-1]);

为什么这个转移成立呢?

①采取某个操作的时候,对应区间的a数组值为1,那么新增的num(a0b1-a0b0)肯定为0(因为没有a0b?的项)。

②在这个转移中,我们把采取某个操作对之后点的影响都集中到最后一点上,也就是只更新dp[b]。根据上一点,按理来说dp[b]=min(dp[b],dp[a-1]);

但是需要注意的是,采取某个操作之后,实际上a~b中间的值也会发生改变。但是我们没有更新。假设ab之间存在下标c,可能存在i之前的操作j,采取了操作j到c。但是这个操作对i-1带来的变化并不会体现在i-1上(因为这个操作下我们只更新c点)。

所以实际上对于一个x点,包括它的操作中的最右端点L,那么x的最小num值为从x到L中的最小num。

③如果纯dp,会发现超时。考虑用线段树来加速“dp[b]=min(dp[b], dp[a-1])=min(dp[b],min(dp[a-1], d[a],...dp[b-1]))”这个操作。由于从i=1开始,a-1可能为0,使线段树操作困难。所以可以直接令nv=min(dp[a-1], 线段树查找(dp[a],dp[a+1]...dp[b-1]))

然后dp[b]=min(dp[b], nv)

 

 1 #include <stdio.h>
 2 #include <vector>
 3 #include <algorithm>
 4 using namespace std;
 5 typedef long long ll;
 6 const ll mx=2e5+10;
 7 const ll inf=1<<30;
 8 inline ll min(ll a, ll b){return a<b?a:b;}
 9 ll N, Q, rt=1;
10 ll b[mx];
11 vector<ll>vec[mx];
12 ll dp[mx];
13 struct node{
14     ll v;
15     ll l, r;
16 }nos[mx<<3];
17 void pushup(ll x){
18     nos[x].l=nos[x<<1].l;nos[x].r=nos[x<<1|1].r;
19     nos[x].v=min(nos[x<<1].v, nos[x<<1|1].v);
20 }
21 void build(ll l, ll r, ll x){
22     if(l==r){
23         nos[x].l=nos[x].r=l;
24         nos[x].v=inf;
25         return;
26     }
27     ll mid=(l+r)>>1;
28     build(l, mid, x<<1);build(mid+1, r, x<<1|1);
29     pushup(x);
30 }
31 ll query(ll l, ll r, ll x){
32     if(l<=nos[x].l && r>=nos[x].r){
33         return nos[x].v;
34     }
35     ll mid=(nos[x].l+nos[x].r)>>1;
36     ll ans=inf;
37     if(l<=mid) ans=min(ans, query(l, r, x<<1));
38     if(r>mid) ans=min(ans, query(l, r, x<<1|1));
39     return ans;
40 }
41 void update(ll x, ll id, ll val){
42     if(nos[x].l==nos[x].r){
43         nos[x].v=val;
44         return;
45     }
46     ll mid=(nos[x].l+nos[x].r)>>1;
47     if(id<=mid)update(x<<1, id, val);
48     else update(x<<1|1, id, val);
49     pushup(x);
50 }
51 void solve(){
52     scanf("%lld", &N);
53     for(ll i=1;i<=N;i++) dp[i]=inf;
54     build(1, N, rt);
55 
56     for(ll i=1;i<=N;i++) scanf("%lld", &b[i]);
57     scanf("%lld", &Q);
58     for(ll i=1;i<=Q;i++){
59         ll l, r;
60         scanf("%lld %lld", &l, &r);
61         vec[l].push_back(r);
62     }
63     dp[0]=0;
64     for(ll i=1;i<=N;i++){
65         for(ll j=0;j<vec[i].size();j++){
66             ll v=vec[i][j];
67             ll nv=dp[i-1];
68             nv=min(nv, query(i, v, rt));
69             if(dp[v]>nv){
70                 dp[v]=nv;
71                 update(rt, v, nv);
72             }
73         }
74         //如果不选
75         ll nv=dp[i-1]+(b[i]==1?1:-1);
76         if(dp[i]>nv){
77             dp[i]=nv;
78             update(rt, i, nv);
79         }
80     }
81     ll base=0;
82     for(ll i=1;i<=N;i++)base+=b[i]==0?1:0;
83     printf("%lld\n", base+dp[N]);
84 }
85 int main(){
86     solve();
87     return 0;
88 }
AC代码

//这个dp好难理解

//好奇妙 今天才知道原来dp可以用线段树加速

posted @ 2021-04-15 08:55  反射狐  阅读(88)  评论(0)    收藏  举报