题目背景
龙年到,帆帆也举起了自己的彩龙灯,他自己也要变成大彩龙啦!
题目描述
帆帆一共有 �n 盏龙灯,第 �i 盏的颜色是 ��ai。
帆帆认为一段区间 [�,�][l,r] 的美观度 �(�,�)f(l,r) 为 ��⋯��al⋯ar 中的不同颜色个数。
帆帆准备带着自己的龙灯去玩,他一共计划去玩 �m 天,第 �i 天,他会带着自己的 1⋯��1⋯xi 号龙灯,但是他发现如果把很多龙灯装在一起,那么别人只会注意到其中有多少种不同的颜色。
因此帆帆准备把这 ��xi 个龙灯按照编号顺序分成恰好 ��ki 个区间,满足每盏灯恰好在一个区间内。
那么帆帆这次出行的美观度就为所有区间的美观度的和。
请你帮帮帆帆最大化每一次出行的美观度。
输入格式
第一行五个整数 �,�,��,����,����n,m,id,seed,limx,分别代表序列长度,询问次数,以及 subtasksubtask 编号(样例中的 ��=0id=0),随机数种子,以及一个和询问有关的参数 ����limx。
因为本题输入量过大,因此采用如下方式生成询问:
我们使用下面的代码生成一个伪随机数列:
uint64_t PRG_state;
uint64_t get_number()
{
PRG_state ^= PRG_state << 13;
PRG_state ^= PRG_state >> 7;
PRG_state ^= PRG_state << 17;
return PRG_state;
}
int readW(int l,int r)
{
return get_number()%(r-l+1)+l;
}
一开始 PRG_state=seed
,你每次调用 readW(l,r)
会返回一个 [�,�][l,r] 内的随机数。
第二行 �n 个整数,第 �i 个整数代表 ��ai。
设 ��,��,��xi,ki,ci 表示第 �i 组询问需要的参数 �,�,�x,k,c,其中 �c 的作用见输出格式,那么所有询问的参数可以用以下程序生成:
for (int i = 1; i <= m; ++i) {
x[i] = readW(limx, n);
k[i] = readW(1, x[i]);
c[i] = readW(0, 1e7);
}
注:请不要在运行上述代码段获得各组询问的参数之前调用 readW()
函数,否则无法获得正确的询问信息。 本题不需要利用该伪随机数生成器的特殊性质,你只需将 get_number()
视为一个每次调用独立且均匀随机地生成一个无符号 6464 位整数的生成器即可。本题也不需要优化伪随机数生成器的内部实现。
输出格式
为了减少输出量,我们使用如下方式进行信息压缩:
如果第 �i 组询问的答案为 ����ansi,其生成参数为 ��ci,那么你需要输出:
⨁�=1�(����×��)i=1⨁m(ansi×ci)
其中 ⨁⨁ 为异或运算,该值显然一定位于 long long
能表示的整数范围内。
输入输出样例
输入 #1复制
5 5 0 956144375 1 2 4 1 5 2
输出 #1复制
21971409
输入 #2复制
10 10 0 478178732 1 2 2 1 1 2 1 2 1 2 1
输出 #2复制
2834792
说明/提示
【样例1解释】
询问分别是:
3 1 6121576
5 3 3089509
1 1 4506170
3 1 2821007
1 1 7941511
答案分别是:
3
5
1
3
1
对于第一组询问,要分成一个区间,那么就是 [1,3][1,3],美观度就是 �(1,3)=3f(1,3)=3 。
对于第二组询问,最优的方案是分成 [1,3],[4,4],[5,5][1,3],[4,4],[5,5],美观度是 �(1,3)+�(4,4)+�(5,5)=5f(1,3)+f(4,4)+f(5,5)=5
后三个询问同理。
【样例2解释】
询问分别是:
8 4 6858024
3 2 236530
2 2 8140891
5 3 4562139
8 7 4749403
7 4 4319971
5 1 5063575
3 1 7343109
6 2 1566851
3 1 7959241
询问答案分别是:
7
3
2
5
8
7
2
2
4
2
【数据范围】
本题采用捆绑测试。
- 子任务一(1010 分):1≤�≤5001≤n≤500。
- 子任务二(1515 分):1≤�≤30001≤n≤3000。
- 子任务三(1515 分):�=1m=1。
- 子任务四(2020 分):1≤��≤301≤ai≤30。
- 子任务五(2020 分):1≤�≤4×1041≤n≤4×104。
- 子任务六(2020 分):无特殊限制。
对于 100%100% 的数据,1≤�≤1051≤n≤105,1≤�≤1061≤m≤106,0≤����≤1090≤seed≤109,1≤����≤�1≤limx≤n,1≤��≤�1≤ai≤n。
subtask 1
设 ���,�dpi,j 表示把前 �i 个位置划分成 �j 段的最小代价,转移:
���,�=max�=1����−1,�−1+�(�,�)dpi,j=k=1maxidpk−1,j−1+f(k,i)
其中 �(�,�)f(k,i) 为区间 [�,�][k,i] 内的颜色数,可以预处理出来。
询问是 �(1)O(1) 的,总复杂度 �(�3+�)O(n3+m)。
subtask 2subtask 2
有至少两个做法。
sol 1
优化第一部分的 DP,以 �j 为阶段,那么我们只需要维护 ���−1,�−1+�(�,�)dpk−1,j−1+f(k,i) 的最大值。
开一棵线段树,第 �k 个位置维护 ��=���−1,�−1+�(�,�)gk=dpk−1,j−1+f(k,i),当我们 �→�+1i→i+1 时:
- ��+1=���−1,�gk+1=dpj−1,k
- ∀�>����+1,��=��+1∀k>lsti+1,gk=gk+1
其中 ����lsti 为 �i 左边第一个等于 ��ai 的数的下标,没有则为 00。
上面的转移只需要线段树支持区间加即可。
询问还是 �(1)O(1) 的,总复杂度 �(�2log�+�)O(n2logn+m)。
sol 2
我们不难证明 �(�,�)f(k,i) 满足四边形不等式,因此 DP 可以用决策单调性优化。
直接分治,两个指针移动来维护 �(�,�)f(k,i),总复杂度 �(�2log�+�)O(n2logn+m)。
subtask 3subtask 3
因为 �=1m=1,所以我们只需要解决一个询问。
由于蒙日矩阵 max+max+ 卷积的 �k 次幂的每个位置都关于 �k 凸,因此 ���,�dpi,j 关于 �j 是一个凸函数,这种区间划分问题的经典做法是 ���wqs 二分。
二分斜率 �c,转移为:
���=max����−1+�(�,�)−�dpi=kmaxdpk−1+f(k,i)−c
然后同上用线段树优化转移,复杂度 �(�log2�+�)O(nlog2n+m)。
subtask 4subtask 4
我们考虑一下 ���wqs 二分的斜率 �c。
那么因为 ��≤30ai≤30,所以 �(�,�)≤30f(k,i)≤30,那么当 �>30c>30 时,�(�,�)−�<0f(k,i)−c<0,此时切点一定是分一段,因此只有 �≤30c≤30 的 �c 有意义。
对于 �=0⋯30c=0⋯30 各做一遍线段树优化 DP,存下每个前缀的答案即可。
复杂度 �(max���log�+�)O(maxainlogn+m)。
subtask 5subtask 5
上一个做法告诉我们,当 �c 过大时,切点就会很小。
我们可以具体分析一下,设 �(�)=��∗,�,�(�)=�(�)−�(�−1)F(k)=dp∗,k,D(k)=F(k)−F(k−1),因为 �F 是凸函数,所以 �(�)≥�(�+1)D(k)≥D(k+1) 恒成立。
同时,因为 �(�)≤�F(k)≤n,那么有 (�−1)�(�)≤∑�=2��(�)≤�(�)−�(1)≤�(k−1)D(k)≤∑i=2kD(i)≤F(k)−F(1)≤n,即 �(�)≤⌊��−1⌋D(k)≤⌊k−1n⌋。
设斜率为 �c 时的切点为 �(�)G(c) ,那么 �(�(�)−1)−�×(�(�)−1)≤�(�(�))−�×�(�)F(G(c)−1)−c×(G(c)−1)≤F(G(c))−c×G(c) ,即 �(�(�))−�(�(�)−1)≥�F(G(c))−F(G(c)−1)≥c ,�≤�(�(�))≤⌊��(�)−1⌋c≤D(G(c))≤⌊G(c)−1n⌋。
我们取阈值 �B,
-
对于 �≤�k≤B 的询问,我们可以预处理所有普通 DP 值。
-
对于 �>�k>B 的询问,根据上面的性质,�(�)≥�G(c)≥k 的 �c 满足 �≤⌊��⌋c≤⌊Bn⌋ ,我们预处理这些 �c 对应的 DP 值。
都采用线段树优化,单次 DP 都是 �(�log�)O(nlogn),取 �=�B=n,我们预处理的复杂度是 �(��log�)O(nnlogn)。
询问时,前部分可以 �(1)O(1) 回答,后半部分可以直接二分,不过这样空间是 �(��)O(nn) 的。
同时注意到 �(�)G(c) 是单调的,因此可以把询问按照 �k 排序后,用一个指针维护转移点,空间就是线性了。
排序可以用桶排序,该做法时间复杂度 �(��log�+�)O(nnlogn+m),空间复杂度 �(�+�)O(n+m)。
注意到线段树的常数比较大,如果被卡常可以把第一部分的 ��dp 换成决策单调性分治,因为访问时连续的所以常数小很多。如果实现较好,也可以直接获得满分。
subtask 6subtask 6
考虑能不能把线段树的 loglog 去掉。
观察一下我们实际需要支持的操作:
- 向末尾加入一个数
- 后缀加 11
- 求最大值
维护一个单调递减的单调栈,那么 11 操作可以直接不断弹栈维护,33 操作就是查询栈底元素。
对于 22 操作,我们找到该后缀在单调栈上对应的位置,这个可以用并查集维护,然后相当于把这个部分往前面合并弹出若干元素,最后打上 +1+1 标记。
因为要支持中间弹出元素,所以我们用链表维护这个单调栈,至于 +1+1 标记,我们可以发现我们相当于只需要查询栈底,查询栈顶,查询某相邻两个位置的差值,因此直接维护单调栈内元素的差分值即可,打标记是简单的。
瓶颈在于并查集,因此单次 ��dp 的复杂度优化到了 �(��(�))O(nα(n)),如果采用严格线性并查集,我们可以做到 �(�)O(n)。
剩下部分不变,复杂度 �(��+�)O(nn+m),空间 �(�+�)O(n+m),可以通过此题。
值得一提的是,在本题中你可以发现,我们优化单次 DP 和优化多次询问的部分是独立的,也就是说,我们把 �(�,�)f(l,r) 换成任意凸函数,在值域不大的情况下都可以在根号的代价内求出所有函数值。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+7;
typedef long long LL;
uint64_t PRG_state;
uint64_t get_number()
{
PRG_state ^= PRG_state << 13;
PRG_state ^= PRG_state >> 7;
PRG_state ^= PRG_state << 17;
return PRG_state;
}
int readW(int l,int r)
{
return get_number()%(r-l+1)+l;
}
int n,m,tid,limx;
int a[N],lst[N],al[N];
#define PII pair<int,int>
#define mk(x,y) make_pair(x,y)
PII f[N],g[N];
PII operator +(PII A,PII B){return mk(A.first+B.first,A.second+B.second);}
PII operator -(PII A,PII B){return mk(A.first-B.first,A.second-B.second);}
vector<PII> qry[N];
int nxt[N],pre[N],jump[N];
PII tag[N],Wl,Wr;
int get(int x)
{
if(x==jump[x])return x;
return jump[x]=get(jump[x]);
}
inline int del(int x)
{
int p=pre[x];
nxt[pre[x]]=nxt[x];
pre[nxt[x]]=pre[x];
pre[x]=nxt[x]=0;
return p;
}
inline void push(int x,PII v)
{
tag[x]=mk(0,0);
jump[x]=x;
pre[x]=x-1;
nxt[x-1]=x;
nxt[x]=0;
int l=x-1;
while(l)
{
if(Wr>v)break;
Wr=Wr-tag[l];
jump[l]=x;
l=del(l);
}
if(!l) Wl=Wr=v;
else
{
nxt[l]=x;
pre[x]=l;
tag[x]=v-Wr;
Wr=v;
}
}
void update(int x)
{
x=get(x);
int l=pre[x];
while(l)
{
if(!(tag[x].first==0||(tag[x].first==-1&&tag[x].second>0)))break;
jump[l]=x;
tag[x]=tag[x]+tag[l];
l=del(l);
}
if(!l)
{
Wl=Wl+tag[x];
tag[x]=mk(0,0);
Wl.first++;
Wr.first++;
}
else
{
nxt[l]=x;
pre[x]=l;
tag[x].first++;
Wr.first++;
}
}
const int inf = 1e7;
int now[N],siz[N],len[N];
int main()
{
cin>>n>>m>>tid>>PRG_state>>limx;
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
al[i]=lst[a[i]];
lst[a[i]]=i;
}
for(int i=1;i<=m;i++)
{
int x=readW(limx,n);
int k=readW(1,x);
int c=readW(0,1e7);
qry[x].push_back(mk(k,c));
}
for(int i=1;i<=n;i++)sort(qry[i].begin(),qry[i].end()),len[i]=(int)qry[i].size();
LL ans=0;
for(int i=0;i<=n;i++)f[i]=g[i]=mk(-inf,-inf);
g[0]=mk(0,0);
int up=0;
for(int k=1;;k++)
{
for(int i=1;i<=n;i++)
{
push(i,g[i-1]);
update(al[i]+1);
f[i]=Wl;
}
up=f[n].first-g[n].first;
for(int i=0;i<=n;i++)
{
g[i]=f[i];
f[i]=mk(-inf,0);
}
for(int i=k;i<=n;i++)
{
while(now[i]<len[i]&&qry[i][now[i]].first==k)
{
ans^=(1ll*qry[i][now[i]].second*g[i].first);
now[i]++;
}
}
if(k>1&&up*1<=k)break;
}
for(int i=1;i<=n;i++)siz[i]=qry[i].size()-1;
for(int w=0;w<=up;w++)
{
f[0]=mk(0,0);
int flag=0;
for(int i=1;i<=n;i++)
{
push(i,f[i-1]);
update(al[i]+1);
f[i]=Wl+mk(-w,-1);
while(siz[i]>=now[i]&&qry[i][siz[i]].first>=-f[i].second)
{
ans^=(1ll*qry[i][siz[i]].second*(f[i].first+w*qry[i][siz[i]].first));
siz[i]--;
}
if(siz[i]<now[i])flag++;
}
if(flag==n)break;
}
cout<<ans;
return 0;
}
拜拜!