51nod最大M子段和系列(51nod1052,51nod1053&51nod1115)

题目1:51nod1052.
题目大意:给定一个 n n n个数字组成的序列 A A A,在 A A A中选出 M M M个不相交的子段,使得这 M M M个子段的子段和之和最大, 1 ≤ n ≤ 5000 1\leq n \leq 5000 1n5000.

很显然可以想到一个很简单的DP,用 f [ i ] [ j ] f[i][j] f[i][j]表示前 i i i个数中选 j j j段的且最后一段必须包括第 i i i个数的最优解.然后就可以得到一个最简单的转移方程:
f [ i ] [ j ] = m a x k = 1 i − 1 { f [ k ] [ j − 1 ] + m s [ k + 1 ] [ i ] } f[i][j]=max_{k=1}^{i-1} \{ f[k][j-1]+ms[k+1][i]\} f[i][j]=maxk=1i1{f[k][j1]+ms[k+1][i]}

其中 m s [ i ] [ j ] ms[i][j] ms[i][j]表示从序列 A A A i i i j j j这一段中最大的后缀和(和最大的必须包括 j j j的子段).
这个算法的时间复杂度为 O ( n 3 ) O(n^3) O(n3),并不能通过这道题.

我们试着改进这个方程,发现其实可以这样转移:
f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] + A [ i ] , m a x k = 1 i − 1 { f [ k ] [ j − 1 ] } + A [ i ] ) f[i][j]=max(f[i-1][j]+A[i],max_{k=1}^{i-1}\{ f[k][j-1] \}+A[i]) f[i][j]=max(f[i1][j]+A[i],maxk=1i1{f[k][j1]}+A[i])

仔细一想发现 m a x k = 1 i − 1 { f [ k ] [ j − 1 ] } max_{k=1}^{i-1}\{ f[k][j-1] \} maxk=1i1{f[k][j1]}可以在DP的同时通过前缀 m a x max max来做到 O ( 1 ) O(1) O(1)回答,然后就做到了 O ( 1 ) O(1) O(1)转移,综合时间复杂度降为 O ( n 2 ) O(n^2) O(n2).

不过看起来这道题卡空间,用滚动数组不能优化第一层状态,所以要优化第二层,枚举时也先枚举第二层.

代码如下:

#include<bits/stdc++.h>
  using namespace std;
 
#define Abigail inline void
typedef long long LL;
 
const int N=5000;
const LL INF=(1LL<<50)-1LL;
 
LL f[N+9][2],F[N+9][2],a[N+9],n,m,old,now; 
 
Abigail into(){
  scanf("%d%d",&n,&m);
  for (int i=1;i<=n;++i)
    scanf("%lld",&a[i]);
}
 
Abigail work(){
  old=1;now=0;
  for (int j=1;j<=m;++j){
    for (int i=1;i<=n;++i)
      f[i][now]=-INF;
    for (int i=1;i<=n;++i)
      f[i][now]=max(f[i-1][now],F[i-1][old])+a[i],F[i][now]=max(f[i][now],F[i-1][now]);
    now^=1;old^=1;
  }
}
 
Abigail outo(){
  printf("%lld\n",F[n][old]);
}
 
int main(){
  into();
  work();
  outo();
  return 0;
}

题目2:51nod1053.
题目大意:题目1的数据加强版, 1 ≤ n ≤ 50000 1\leq n \leq 50000 1n50000.

我们发现DP貌似怎么优化也过不了这题了…

考虑这个问题的性质,发现这个问题中如果没有 m m m的限制的最好情况肯定是将所有正数和0取出.

有了 m m m的限制后,发现取出的 m m m段中任何一段 [ l , r ] [l,r] [l,r]一定满足 A [ l ] A[l] A[l] A [ l − 1 ] A[l-1] A[l1]异号且 A [ r ] A[r] A[r] A [ r + 1 ] A[r+1] A[r+1]异号,于是考虑将原序列 A A A转化为正负交替的形式 B B B,例如序列 1 &ThinSpace; 3 &ThinSpace; − 2 &ThinSpace; 2 &ThinSpace; − 3 &ThinSpace; − 1 &ThinSpace; 0 &ThinSpace; 9 1\,3\,-2\,2\,-3\,-1\,0\,9 13223109就可以处理成 4 &ThinSpace; − 2 &ThinSpace; 2 &ThinSpace; − 4 &ThinSpace; 9 4\,-2\,2\,-4\,9 42249.

那么现在序列 B B B中每一个数都代表了序列 A A A中一段正数或负数的和,且序列 B B B中每一个数B[i]都满足与 B [ i − 1 ] B[i-1] B[i1]异号且与 B [ i + 1 ] B[i+1] B[i+1]异号.

很明显,记 B B B序列中正数的个数为 c n t cnt cnt,当 m ≥ c n t m\geq cnt mcnt时,就是将所有 B B B中正数相加.而 m &lt; c n t m&lt;cnt m<cnt时,我们就需要在 B B B序列中所有正数的基础上,去掉一些正数或加入一些负数,来使得总段数等于 m m m.

也就是说,当 m &lt; c n t m&lt;cnt m<cnt时,我们需要维护序列 C C C初始为 B B B中所有元素取绝对值,即 c [ i ] = ∣ B [ i ] ∣ c[i]=|B[i]| c[i]=B[i].然后我们需要做 c n t − m cnt-m cntm次操作,每一次操作是将 C C C中最小的数 C [ i ] C[i] C[i]删除,并将 C [ i − 1 ] C[i-1] C[i1] C [ i + 1 ] C[i+1] C[i+1]合并.同时记录一个 a n s ans ans初始为 B B B中所有正数之和,然后每次操作时 a n s ans ans减去 C [ i ] C[i] C[i],最后的 a n s ans ans就是答案.

仔细思考一下就会发现上面这个过程十分正确且巧妙.

那么现在我们只需要找到一个数据结构维护 C C C序列即可,很容易想到堆是个不错的选择.但是堆不能完成删除与合并两个相邻元素的操作,于是我们就可以直接用set代替堆进行删除,同时维护一个双向链表来维护合并.时间复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn).

代码如下:

#include<bits/stdc++.h>
  using namespace std;

#define Abigail inline void
typedef long long LL;
#define pair pair<LL,int>
#define mp make_pair<LL,int>
#define fi first
#define se second
 
const int N=50000;
const LL INF=(1LL<<50)-1LL;
 
struct List{
  int next,last;
}q[N+9];
int top,vis[N+9];
set<pair> s;
 
void Erase(int x){
  int l=q[x].last,r=q[x].next;
  if (l) q[l].next=r;
  if (r) q[r].last=l;
}
 
int n,m,cnt,num;
LL a[N+9],b[N+9],ans;
 
void Get_B(){
  b[cnt=1]=a[1];
  for (int i=2;i<=n;++i)
    a[i-1]<0^a[i]<0?b[++cnt]=a[i]:b[cnt]+=a[i];
  for (int i=1;i<=cnt;++i)
    if (b[i]>=0) ans+=b[i],++num;
}
 
void solve(){
  if (num<=m) return;
  for (int i=1;i<=cnt;++i)
    s.insert(mp(abs(b[i]),i));
  for (int i=1;i<=cnt;++i)
    q[i].last=i-1,q[i].next=i+1;
  q[cnt].next=0;
  num-=m;
  pair tmp;int l,r,x;
  while (num){
    tmp=*s.begin();x=tmp.se;
    s.erase(*s.begin());
    l=q[x].last;r=q[x].next;
    if (b[x]<0&&(!l||!r)) continue;      //若当前得到的是一个在边界的负数就不管,这个边界问题比较毒瘤 
    s.erase(mp(abs(b[l]),l));s.erase(mp(abs(b[r]),r));
    ans-=abs(b[x]);
    b[x]+=b[l]+b[r];
    Erase(l);Erase(r);
    s.insert(mp(abs(b[x]),x));
    --num;
  }
}
 
Abigail into(){
  scanf("%d%d",&n,&m);
  for (int i=1;i<=n;++i)
    scanf("%lld",&a[i]);
}
 
Abigail work(){
  Get_B();
  solve();
}
 
Abigail outo(){
  printf("%lld\n",ans);
}
 
int main(){
  into();
  work();
  outo();
  return 0;
}

题目3:51nod1115.
题目大意:在题目1的基础上,序列变成了环状,并且 1 ≤ n ≤ 100000 1 \leq n \leq 100000 1n100000.

序列变成了环状怎么办?我们发现双向链表是可以首尾相连变成循环链表来解决环的问题的,而且这个时候就不存在处在边界上的负数这种情况了,其实会比上面的题好写很多.

代码如下:

#include<bits/stdc++.h>
  using namespace std;
 
#define Abigail inline void
typedef long long LL;
#define pair pair<LL,int>
#define mp make_pair<LL,int>
#define fi first
#define se second
 
const int N=100000;
 
struct List{
  int next,last;
}q[N+9];
int top,n,m,cnt,num;
LL a[N+9],b[N+9],ans;
set<pair> s;
 
void Erase(int x){
  q[q[x].next].last=q[x].last;
  q[q[x].last].next=q[x].next;
}
 
void Get_B(){
  b[cnt=1]=a[1];
  for (int i=2;i<=n;++i)
    a[i-1]<0^a[i]<0?b[++cnt]=a[i]:b[cnt]+=a[i];
  if (b[1]<0==b[cnt]<0) b[1]+=b[cnt--];
  for (int i=1;i<=cnt;++i)
    if (b[i]>=0) ++num,ans+=b[i];
}
 
void solve(){
  if (num<=m) return;
  for (int i=1;i<=cnt;++i){
    q[i].last=i-1,q[i].next=i+1;
    s.insert(mp(abs(b[i]),i));
  }
  q[1].last=cnt,q[cnt].next=1;
  num-=m;
  pair tmp;int l,r,x;
  while (num){
    tmp=*s.begin();s.erase(*s.begin());
    x=tmp.se;l=q[x].last;r=q[x].next;
    if (b[x]<=0&&(!l||!r)) continue;
    s.erase(mp(abs(b[l]),l));s.erase(mp(abs(b[r]),r));
    ans-=abs(b[x]);
    b[x]+=b[l]+b[r];
    Erase(l);Erase(r);
    s.insert(mp(abs(b[x]),x));
    --num;
  }
}
 
Abigail into(){
  scanf("%d%d",&n,&m);
  for (int i=1;i<=n;++i)
    scanf("%lld",&a[i]);
}
 
Abigail work(){
  Get_B();
  solve();
}
 
Abigail outo(){
  printf("%lld\n",ans);
}
 
int main(){
  into();
  work();
  outo();
  return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值