题目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
1≤n≤5000.
很显然可以想到一个很简单的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=1i−1{f[k][j−1]+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[i−1][j]+A[i],maxk=1i−1{f[k][j−1]}+A[i])
仔细一想发现 m a x k = 1 i − 1 { f [ k ] [ j − 1 ] } max_{k=1}^{i-1}\{ f[k][j-1] \} maxk=1i−1{f[k][j−1]}可以在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
1≤n≤50000.
我们发现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[l−1]异号且 A [ r ] A[r] A[r]和 A [ r + 1 ] A[r+1] A[r+1]异号,于是考虑将原序列 A A A转化为正负交替的形式 B B B,例如序列 1   3   − 2   2   − 3   − 1   0   9 1\,3\,-2\,2\,-3\,-1\,0\,9 13−22−3−109就可以处理成 4   − 2   2   − 4   9 4\,-2\,2\,-4\,9 4−22−49.
那么现在序列 B B B中每一个数都代表了序列 A A A中一段正数或负数的和,且序列 B B B中每一个数B[i]都满足与 B [ i − 1 ] B[i-1] B[i−1]异号且与 B [ i + 1 ] B[i+1] B[i+1]异号.
很明显,记 B B B序列中正数的个数为 c n t cnt cnt,当 m ≥ c n t m\geq cnt m≥cnt时,就是将所有 B B B中正数相加.而 m < c n t m<cnt m<cnt时,我们就需要在 B B B序列中所有正数的基础上,去掉一些正数或加入一些负数,来使得总段数等于 m m m.
也就是说,当 m < c n t m<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 cnt−m次操作,每一次操作是将 C C C中最小的数 C [ i ] C[i] C[i]删除,并将 C [ i − 1 ] C[i-1] C[i−1]与 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
1≤n≤100000.
序列变成了环状怎么办?我们发现双向链表是可以首尾相连变成循环链表来解决环的问题的,而且这个时候就不存在处在边界上的负数这种情况了,其实会比上面的题好写很多.
代码如下:
#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;
}