题意

给定一个长度为n的序列A[],你需要确定一个长度为n的排列P[],定义当前排列的值为:

\[\sum_{i=1}^{n}{A[i]P[i]}
\]

现在给定一个整数k,需要你求出,在所有可能的排列中(显然有n!种),最小的k个"排列的值"是多少?依次输出。排列不同,值相同的,算不同的方案。

1<=n<=100000

输入格式

第一行为两个整数n,k,含义如题意所示。接下来n行为n个整数,代表A[]数组。

输出格式

输出k个整数,分别表示前k个"排列的值",从小到大输出。

思路

感觉这道题比较神。

这里就不再提及骗分的做法了,直接说正解是怎样的。

首先第一步我都没想到...

可以将题目中的式子转化为:

\[\sum_{i=1}^{n}A_{P_i}(n-i+1)
\]

不仅没想到,光是看懂这一步就花了我不少的时间。如果觉得比较显然的话,可以直接跳过下面的解说:

首先让我们改变一下P[]数组的定义:原来是单纯的一个排列,而现在指的是i这个数字在排列中位于P[i]这个位置。很容易就能够发现这样子是一一对应的,只不过式子就变成了\(\sum_{i=1}^{n}A_{P_i}i\),好像和题解不一样???

但是我们还可以这样看。只要保证了一一对应,就能够映射回去。将上述定义下的P[]数组倒序一下,是不是式子就变对了呢?(根据博主自己的理解,这个时候的P[i]应该是(n-i+1)这个数字在排列中的哪个位置)

将问题转化之后,考虑如何从一个P[]转移到另外一个P'[]来得到连续的k个答案。但是在这之前,我们能够发现,在这种问题的定义下,初始状态下,A[]应该是递增的(因为要求值最小,所以说i=1是应乘一个较小的数)。

然后又是很神奇的一步:

考虑这样的一种转移:对于P[]数组而言,假设开始的t位都已经确定了(不能再改变),那么就考虑剩下的t+1~n个数字。

设一个位置u,一个长度v,保证u-v>t。当前我们令P[u-v]=P[u],并将P[u-v~u-1]这一段区间内的数全部都向后移动一位,同时将t更新为u-v。也就是说整个数组由:P[u-v],P[u-v+1]...P[u]变为了P[u],P[u-v],P[u-v+1]...P[u-1]。

考虑这样变化之后,整个序列的值的增量是多少:P[u-v~u-1]都向右移了一段,而乘的数字是递减的(n,n-1,...3,2,1),所以说减少了\(\sum_{i=u-v}^{u-1}A[P[i]]\);又因为P[u]向左移了u-1-(u-v)+1=v格,所以增加了v*P[u]。将式子合并之后就成了\(\sum_{i=1}^{v}(A_{P_{u}}-A_{P_{u-i}})\)。设一次转移的增量是g(u,v)。由于我们已经发现了A[]是递增的,所以说g(u,v)>=g(u,v+1),也就是说只有取v尽量小的时候是最优的。同样哦我们也能够发现,只有进行一次转移,值才有可能最小,所以说我们只需要考虑一次转移就可以了。

为啥这样子就能够代表所有可能状态的转移呢??下面又是博主的"想象":

假设下一步不是这样子转移的,而是一个"random_shuffle"之后的结果(仍要保证t及其之前的不变)。学过冒泡排序的都知道,这玩意是能够用冒泡排序做到的。而冒排的一次交换操作可以看成是将一个元素放到了另一个元素的前面,那么就成了上面的操作。又因为我们已经证明了一次最多只需要考虑一次操作,所以说就可以归为上面的情况了。

真是刺激

然而一切才刚刚开始...

以上的分析只是一个前置而已...现在我们考虑一个解决这个问题的一个大致思路:通过对初始状态(A[]递增)进行上述的不断的变化,放进优先队列里面,每一次都把队首取出来,作为当前能够找到的最小的值输出,并在这个状态的基础上进行更新。在队列的每一个状态中,我们都需要维护一个可持久化的线段树,来维护区间内最小的g(u,v)以及u,v,还有当前这个区间内可用的A[]的个数s。

下面是一些实现细节:(我能说是照办标程的做法吗?我实在是不会写...)

看不懂的话可以考虑前后结合看...

  1. 开一个数组TMP[]来存一个状态。每一个位置存三个东西,一个叫Now的线段树表示当前这个状态还有那些状态能够使用,主要是为了方便查询下一步最优转移;一个叫Ori(Origin)的线段树,存这个状态最开始被放进队列的的时候长啥样,主要是为了求出下一步的转移后线段树应有的状态,而避免转移之间互相影响。还有一个数字sum,表示Ori对应的那个状态,也就是刚刚被放进队列的时候的序列的
  2. 最开始将A[]从小到大排序之后,将信息存储在TMP[0].Now里面,线段树的位置i表示A[i]-A[i-1],即最小的转移。注意线段树中位置1一定要初始化为INF,因为A[0]是不存在的。
  3. 将TMP[0].Ori初始化为TMP[0].Now
  4. 将TMP[0]放进队列,注意插进去的答案为TMP[0].sum+TMP[0].Now->g,即转移一次之后的答案。
  5. 每一次将队列的头取出来(状态编号为T),输出答案,然后进行更新:
  6. 更新的时候,其实就是将TMP[T].Ori进行了之前已经判断出来的一次转移之后的状态。申请新的状态num,表示上述含义。并将TMP[num].Now的初值赋为TMP[T].Ori,原因上上面已经说过了。然后将u-v-1之前的点全部删掉,因为t向后移了(忘记定义的往前找找)。再将原来的u删掉,因为它被移到u-v之后就在t上了,以后就固定了。再将原来的u-v那个地方的线段树中的值变为INF,因为它前面已经没有数字了。然后为原来的u+1寻找新搭配,因为它原来的搭配u已经被删除了。这样就完成了TMP[num].Now的更新。当然最后不忘将TMP[num].Ori赋为TMP[num].Now,然后再钦定转移后入队。
  7. 当然,如你所料,这还没完。因为要防止重复转移,但又要进行完所有可能的转移,这个时候就要发挥Now的作用了。这里将讲解如何将Now进行更新。首先,之后肯定是不可能再使用g(u,v)进行转移了,所以说我们要将这个状态转移去掉,将线段树中的u位置赋为新的转移方式g(u,v+1)。而根据之前增量公式可见,这个时候只需要加上A[P[u]]-A[P[u-v-1]]就可以了。然后..就没有然后了...最后记得将这个状态也钦点了转移之后入队。

其实第6、7两点可以看成是两种不同的转移,一种是通过TMP[T].Now转移得到的TMP[num].Now得到的新的状态,也就是删去1~t之后的状态,然后入队;还有一种是进行上述转移之后,需要排除重复转移,又要保证枚举所有的转移,所以将TMP[T].Now中的g(u,v)变为g(u,v+1),再重新入队。

可能博主说的有些含糊不清,但是应该比官方题解清晰一些了,具体还请参见代码~~~

代码

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#define MAXN 300000
#define INF 1000000000000000000LL
using namespace std;
typedef long long LL;
typedef pair<LL,int> PII;
struct node
{
node *ch[2];
int u,v,s;
//u,v,g(u,v),s含义参上。u,v,s,都是再这个子区间的意义下的
LL g;
}tree[MAXN*40+5];
node *ncnt=&tree[0],*NIL=&tree[0];
struct ArrNode
{
node *Ori,*Now;
LL sum;
}TMP[MAXN*40+5];//状态节点
int tcnt=0;
LL A[MAXN+5];
int n,k;
priority_queue<PII,vector<PII>,greater<PII> > que;
void Init()
{
ncnt=NIL=&tree[0];
NIL->ch[0]=NIL->ch[1]=NIL;
NIL->u=NIL->v=NIL->s=0;
NIL->g=INF;
}
inline node* NewNode()
{
node *p=++ncnt;
p->ch[0]=p->ch[1]=NIL;
p->u=p->v=p->s=0;
p->g=INF;
return p;
}
void PushUp(node *rt)
{
node *lch=rt->ch[0],*rch=rt->ch[1];
if(lch->g<rch->g)
rt->g=lch->g,rt->u=lch->u,rt->v=lch->v;
else
rt->g=rch->g,rt->u=lch->s+rch->u,rt->v=rch->v;
rt->s=lch->s+rch->s;
}
void Build(node *&rt,int l,int r)
{
rt=NewNode();
rt->s=0;
if(l==r)
{
if(l>1) rt->u=rt->v=rt->s=1,rt->g=A[l]-A[l-1];
else rt->u=rt->v=rt->s=1,rt->g=INF;//注意特殊处理
return;
}
int mid=(l+r)/2;
Build(rt->ch[0],l,mid);
Build(rt->ch[1],mid+1,r);
PushUp(rt);
}
void Insert(node *&rt,int l,int r,int p,LL gval,int sval)
//Insert是+=,而change是直接赋值。
{
node *q=NewNode();
*q=*rt;rt=q;
if(l==r)
{
rt->g+=gval;
rt->u=rt->s=rt->v=sval;
return;
}
int mid=(l+r)/2;
if(p<=rt->ch[0]->s) Insert(rt->ch[0],l,mid,p,gval,sval);
else Insert(rt->ch[1],mid+1,r,p-rt->ch[0]->s,gval,sval);
PushUp(rt);
}
void ChangePoint(node *&rt,int l,int r,int p,LL gval,int sval)//与Insert的区别见上
{
node *q=NewNode();
*q=*rt;rt=q;
if(l==r)
{
rt->g=gval;
rt->u=rt->s=rt->v=sval;
return;
}
int mid=(l+r)/2;
if(p<=rt->ch[0]->s) ChangePoint(rt->ch[0],l,mid,p,gval,sval);
else ChangePoint(rt->ch[1],mid+1,r,p-rt->ch[0]->s,gval,sval);
PushUp(rt);
}
void DelSeg(node *&rt,int l,int r,int sum)
{
if(sum==0)
return;
int mid=(l+r)/2;
node *q=NewNode();
*q=*rt;rt=q;
if(rt->ch[0]->s>sum) DelSeg(rt->ch[0],l,mid,sum);
else DelSeg(rt->ch[1],mid+1,r,sum-rt->ch[0]->s),rt->ch[0]=NIL;
PushUp(rt);
}
LL Query(node *rt,int l,int r,int p)
{
if(l==r)
return A[l];
int mid=(l+r)/2;
if(rt->ch[0]->s>=p) return Query(rt->ch[0],l,mid,p);
else return Query(rt->ch[1],mid+1,r,p-rt->ch[0]->s);
}
void Extend(int T)
{
int Z=++tcnt;
node *&TN=TMP[T].Now,*&TO=TMP[T].Ori; TMP[Z].sum=TMP[T].sum+TN->g;//先算值 TMP[Z].Now=TO;//下面再进行线段树形态的求解
int u=TN->u,v=TN->v,pos=u-v;//根据之前维护的信息求得当前的最优转移
//---------pos-pos+1-------u-----------
//---------pos+1--------u-pos----------
//先删掉u这个点
ChangePoint(TMP[Z].Now,1,n,u,INF,0);
//再删掉前面~pos-1的部分
if(pos-1>=1)
DelSeg(TMP[Z].Now,1,n,pos-1);
//为u+1重新寻找新的搭档
if(v+1<=TMP[Z].Now->s)
{
LL newval=Query(TMP[Z].Now,1,n,v+1)-Query(TMP[Z].Now,1,n,v);
ChangePoint(TMP[Z].Now,1,n,v+1,newval,1);
}
ChangePoint(TMP[Z].Now,1,n,1,INF,1);//原来的pos+1也已经失去了搭档了(因为此时为第一个可用的数)
//更新完成,保存初始状态
TMP[Z].Ori=TMP[Z].Now;
//对于TMP[Z]的更新完成了
que.push(PII(TMP[Z].sum+TMP[Z].Now->g,Z)); if(u-v-1>=1)
{
LL delta=Query(TN,1,n,u)-Query(TN,1,n,u-v-1);
Insert(TN,1,n,u,delta,1);//更新为g(u,v+1)
}
else
Insert(TN,1,n,u,INF,1);
que.push(PII(TMP[T].sum+TMP[T].Now->g,T));//钦定后入队
}
int main()
{
Init();
scanf("%d %d",&n,&k);
for(int i=1;i<=n;i++)
scanf("%lld",&A[i]);
sort(A+1,A+1+n);
Build(TMP[0].Ori,1,n);//赋初值
for(int i=1;i<=n;i++)
TMP[0].sum+=A[i]*(n-i+1);//算最初的答案
TMP[0].Now=TMP[0].Ori;
printf("%lld\n",TMP[0].sum);//先把最小的值输出来
que.push(PII(TMP[0].sum+TMP[0].Now->g,0));//钦定转移并入队
for(int i=1;i<=k-1;i++)
{
PII fro=que.top();
que.pop();
printf("%lld\n",fro.first);//先输出早就钦定好的答案
Extend(fro.second);//再进行钦定后的结果的状态的更新
}
return 0;
}

【2019雅礼集训】【可持久化线段树】【模型转化】D1T2Permutation的更多相关文章

  1. 【2019雅礼集训】【CF 960G】【第一类斯特林数】【NTT&多项式】permutation

    目录 题意 输入格式 输出格式 思路 代码 题意 找有多少个长度为n的排列,使得从左往右数,有a个元素比之前的所有数字都大,从右往左数,有b个元素比之后的所有数字都大. n<=2*10^5,a, ...

  2. 【2019雅礼集训】【最大费用流】【模型转换】D2T3 sum

    目录 题意 输入格式 输出格式 思路 代码 题意 现在你有一个集合{1,2,3,...,n},要求你从中取出一些元素,使得这些元素两两互质.问你能够取出的元素总和最多是多少? 输入格式 一个整数n 输 ...

  3. #6034. 「雅礼集训 2017 Day2」线段游戏 李超树

    #6034. 「雅礼集训 2017 Day2」线段游戏 内存限制:256 MiB时间限制:1000 ms标准输入输出 题目类型:传统评测方式:Special Judge 上传者: 匿名 提交提交记录统 ...

  4. 【loj6034】「雅礼集训 2017 Day2」线段游戏

    #6034. 「雅礼集训 2017 Day2」线段游戏 内存限制:256 MiB 时间限制:1000 ms 标准输入输出 题目类型:传统 评测方式:Special Judge 上传者: 匿名 题目描述 ...

  5. 雅礼集训2019 D7T2 Subsequence

    雅礼集训2019 D7T2 Subsequence 直接贴题解: 平衡树代码: #include<bits/stdc++.h> #define ll long long #define N ...

  6. 「雅礼集训 2017 Day7」跳蚤王国的宰相(树的重心)

    题面 来源 「 雅 礼 集 训 2017 D a y 7 」 跳 蚤 王 国 的 宰 相   传 统 2000   m s 1024   M i B {\tt「雅礼集训 2017 Day7」跳蚤王国的 ...

  7. LOJ.6073.[2017山东一轮集训Day5]距离(可持久化线段树 树链剖分)

    题目链接 就是恶心人的,简单写写了...(似乎就是[HNOI2015]开店?) 拆式子,记\(dis_i\)为\(i\)到根节点的路径权值和,\(Ans=\sum dis_{p_i}+\sum dis ...

  8. 「雅礼集训 2017 Day2」解题报告

    「雅礼集训 2017 Day2」水箱 我怎么知道这种题目都能构造树形结构. 根据高度构造一棵树,在树上倍增找到最大的小于约束条件高度的隔板,开一个 \(vector\) 记录一下,然后对于每个 \(v ...

  9. [LOJ 6029]「雅礼集训 2017 Day1」市场

    [LOJ 6029] 「雅礼集训 2017 Day1」市场 题意 给定一个长度为 \(n\) 的数列(从 \(0\) 开始标号), 要求执行 \(q\) 次操作, 每次操作为如下四种操作之一: 1 l ...

随机推荐

  1. 关于FastDBF库读写ArcGis dbf文件的小bug

    该库托管于GitHub,地址:https://github.com/SocialExplorer/FastDBF 贡献者应该都是老外,所以…… 1.解析文件头,字段名部分如果有中文命名字段会出错 在D ...

  2. [算法竞赛入门经典] 象棋 ACM/ICPC Fuzhou 2011, UVa1589 较详细注释

    Description: Xiangqi is one of the most popular two-player board games in China. The game represents ...

  3. step into,step over,step out.

    step into:单步执行,遇到子函数就进入并且继续单步执行(简而言之,进入子函数): step over:在单步执行时,在函数内遇到子函数时不会进入子函数内单步执行,而是将子函数整个执行完再停止, ...

  4. 2、jQuery的Ajax简单示例

    一.创建一个JavaWEBProject 二.创建一个Servlet(下面的例子中用到了Json--第32行,Json的作用是将一个Object类转换为特定格式的字符串,如果不用Json直接返回字符串 ...

  5. order by 的用法

    select * from emp order by sal desc  --将员工工资按照由高到低的顺序排列

  6. centos 7.2 下 nginx 1.14.1 安装部署

    Nginx1.14.1安装部署 1.环境: 所有源码在跳板机kx的/web/soft下 2.安装依赖: [root@bogon src]# yum install -y libxml2 openssl ...

  7. mysql并发控制之快照读和当前读

    上一篇简单的介绍了下MVCC(多版本并发控制)的原理,MVCC会对事物内操作的数据做多版本控制,从而实现并发环境下事物对数据写操作的阻塞不影响读操作的性能.而这个多版本控制的实现是由undo log来 ...

  8. Spring ES

    elasticsearchTemplate 和 ElasticsearchRepository JPA中有个ElasticsearchRepository可以做Elasticsearch的相关增删改查 ...

  9. Pytorch--Dropout笔记

    dropout常常用于抑制过拟合,pytorch也提供了很方便的函数.但是经常不知道dropout的参数p是什么意思.在TensorFlow中p叫做keep_prob,就一直以为pytorch中的p应 ...

  10. # 20175333曹雅坤《Java程序设计》第七周学习总结

    教材学习内容总结 第八章-常用实用类String类 构造String对象 字符串的并置 String类的常用方法 字符串与基本数据的互相转化 对象的字符串表示 字符串与字符.字节数组 正则表达式及字符 ...