给定长为
n
n
n(1e5)的数列
s
s
s(1e4),将其分成若干连续段。
可以从一段中任选一个数
s
0
s_0
s0,这一段的贡献就是
c
n
t
[
s
0
]
2
∗
s
0
cnt[s_0]^2*s_0
cnt[s0]2∗s0,其中
c
n
t
[
s
0
]
cnt[s_0]
cnt[s0]表示这一段中
s
0
s_0
s0出现的次数。
求所有段的贡献之和的最大值。
一个基础的dp:
设
d
i
d_i
di表示把前
i
i
i个数分好的最大贡献,
d
0
=
0
d_0=0
d0=0.
d
i
=
m
a
x
{
d
j
−
1
+
c
a
l
(
j
,
i
)
}
d_i = max\{d_{j-1}+cal(j,i)\}
di=max{dj−1+cal(j,i)},其中
0
<
j
<
=
i
0<j<=i
0<j<=i,
c
a
l
(
a
,
b
)
cal(a,b)
cal(a,b)表示把
a
a
a到
b
b
b分成一段的最大贡献。
一个明显的问题是 c a l ( j , i ) cal(j,i) cal(j,i)难以计算,强行计算的复杂度可能达到 O ( n 3 ) O(n^3) O(n3)
结论:最优情况下,每一段的首尾数字必定相同,而且作为
s
0
s_0
s0。
证明:如果存在一个段的首部数字不是这个段的
s
0
s_0
s0,那么可以将其独立成段,贡献显然增加。尾部同理。
把 n n n个位置按值分组,记 p i , j p_{i,j} pi,j表示数值 i i i在原数列中第 j j j次(从第0次开始算)出现的位置,那么有: s [ p i , j ] = i s[p_{i,j}]=i s[pi,j]=i。
此时对于位置
p
i
,
j
p_{i,j}
pi,j,只应该从
p
i
,
j
−
1
、
p
i
,
j
−
1
−
1
、
p
i
,
j
−
2
−
1...
、
p
i
,
0
−
1
p_{i,j}-1、p_{i,j-1}-1、p_{i,j-2}-1...、p_{i,0}-1
pi,j−1、pi,j−1−1、pi,j−2−1...、pi,0−1去转移,且数值
i
i
i出现的次数很容易算出,即:
d
[
p
i
,
j
]
=
m
a
x
{
d
[
p
i
,
k
−
1
]
+
i
∗
(
j
−
k
+
1
)
2
}
d[p_{i,j}] = max\{d[p_{i,k}-1]+i*(j-k+1)^2\}
d[pi,j]=max{d[pi,k−1]+i∗(j−k+1)2},
0
<
=
k
<
=
j
0<=k<=j
0<=k<=j
保存每个位置对应的 i , j i,j i,j值,按 p i , j p_{i,j} pi,j递增的顺序转移即可。
此时复杂度并不能完全满足需求,如果所有数值都相同还是会卡到 O ( n 2 ) O(n^2) O(n2),还需要进一步优化。
int xi[M], xj[M];
vector<int> pos[10016];
ll dp[M];
int main()
{
int n = read();
for(int p=1; p<=n; ++p)
{
xi[p] = read();
xj[p] = pos[xi[p]].size();
pos[xi[p]].push_back(p);
}
for(int p=1; p<=n; ++p)
{
int i = xi[p], j = xj[p];
for(int k=0; k<=j; ++k)
dp[p] = max(dp[p], dp[pos[i][k]-1] + 1ll*i*(j-k+1)*(j-k+1));
}
cout << dp[n] << "\n";
}
开O2直接AC了你敢信
以下内容需要斜率优化作为前置知识,可以自行了解或者察看我的博客qwq.
对于转移式: d [ p i , j ] = m a x { d [ p i , k − 1 ] + i ∗ ( j − k + 1 ) 2 } d[p_{i,j}] = max\{d[p_{i,k}-1]+i*(j-k+1)^2\} d[pi,j]=max{d[pi,k−1]+i∗(j−k+1)2}
整理得到: d [ p i , j ] = i ( j + 1 ) 2 + m a x { d [ p i , k − 1 ] + i k 2 − 2 i ( j + 1 ) k } d[p_{i,j}] = i(j+1)^2 +max\{d[p_{i,k}-1]+ik^2-2i(j+1)k\} d[pi,j]=i(j+1)2+max{d[pi,k−1]+ik2−2i(j+1)k}
设 a < b < j a<b<j a<b<j,那么从 d [ p i , b − 1 ] d[p_{i,b}-1] d[pi,b−1]转移比从 d [ p i , a − 1 ] d[p_{i,a}-1] d[pi,a−1]转移更优等价于
d [ p i , b − 1 ] + i b 2 − 2 i ( j + 1 ) b > d [ p i , a − 1 ] + i a 2 − 2 i ( j + 1 ) a d[p_{i,b}-1]+ib^2-2i(j+1)b>d[p_{i,a}-1]+ia^2-2i(j+1)a d[pi,b−1]+ib2−2i(j+1)b>d[pi,a−1]+ia2−2i(j+1)a
整理得到: ( d [ p i , b − 1 ] + i b 2 ) − ( d [ p i , a − 1 ] + i a 2 ) b − a > 2 i ( j + 1 ) \frac{(d[p_{i,b}-1]+ib^2) - (d[p_{i,a}-1]+ia^2)}{b-a}>2i(j+1) b−a(d[pi,b−1]+ib2)−(d[pi,a−1]+ia2)>2i(j+1)
由斜率优化原理, y a = d [ p i , a − 1 ] + i a 2 , x a = a , k = 2 i ( j + 1 ) y_a=d[p_{i,a}-1]+ia^2,x_a=a,k=2i(j+1) ya=d[pi,a−1]+ia2,xa=a,k=2i(j+1)。大于号,维护上凸包。上凸包斜率递减,目标斜率递增,队尾操作。本质上是一个单调栈。
注意,需要给每一个 i i i值都维护一个独立的凸包。
实现细节:
- 单调栈中可以存放实际位置, i i i和 j j j可以由推导得到。
- 此题中求
d
p
[
p
]
dp[p]
dp[p]时需要把
p
p
p放入凸包中,所以操作顺序应该是:
- 准备放入p,把构成下凸点的栈顶弹出
- 放入p
- 更新目标斜率,把不满足目标斜率的栈顶弹出
- 求dp[p]
AC代码如下:
ll xi[M], xj[M];
vector<int> pos[10016];
ll dp[M];
vector<int> ms[10016]; //单调栈
inline int t1(const vector<int> &vc){return vc[vc.size()-1];} //栈顶的第一个元素
inline int t2(const vector<int> &vc){return vc[vc.size()-2];} //栈顶的第二个元素
inline ll subx(int p1, int p2){return xj[p2]-xj[p1];}
inline ll suby(int p1, int p2){
return (dp[p2-1] + xi[p2]*xj[p2]*xj[p2]) - (dp[p1-1] + xi[p1]*xj[p1]*xj[p1]);
}
inline ll cal(int p, int lp){
return dp[lp-1] + xi[p]*(xj[p]-xj[lp]+1)*(xj[p]-xj[lp]+1);
}
int main(void)
{
int n = read();
for(int p=1; p<=n; ++p)
{
xi[p] = read();
xj[p] = pos[xi[p]].size();
pos[xi[p]].push_back(p);
}
for(int p=1; p<=n; ++p)
{
int i = xi[p], j = xj[p];
while(ms[i].size()>=2 &&
suby(t2(ms[i]),t1(ms[i]))*subx(t1(ms[i]),p) <= subx(t2(ms[i]),t1(ms[i]))*suby(t1(ms[i]),p)
) ms[i].pop_back();
ms[i].push_back(p);
while(ms[i].size()>=2 && suby(t2(ms[i]),t1(ms[i])) <= 2ll*i*(j+1)*subx(t2(ms[i]),t1(ms[i])))
ms[i].pop_back();
dp[p] = cal(p, t1(ms[i]));
}
cout << dp[n] << "\n";
return 0;
}