NRE 枚举dp+线段树加速
题目:https://vjudge.net/contest/433343#problem/A
题意:给定01序列b,和一个全为0的等长的序列a。有Q种操作,每个操作有l和r,可以让a中下标l到r的0变成1。每个操作可用可不用。问操作完(可以甚至一个操作都不做)后ab对应下标数据不同的一共有多少组。
思路:将题目转化为求对应下标下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 }
//这个dp好难理解
//好奇妙 今天才知道原来dp可以用线段树加速