#第一次在CSDN上发博客,博主目前是小菜鸟,这两天在洛谷刷题学习离散化时遇到了一道个人认为很有思维强度的一道题,决定学习大佬们分享博客的习惯,开始自己的学习记录博客之旅#
题目描述
需求分析
看到题目运行我们进行的操作是将数列中的某一段的和加在区间两侧的两个元素上,然后区间端点的两个元素减去这个区间和,所以我们很容易想到前缀和,我们的最终目的是用最少的操作次数使得任意区间和为正
思路分析
之所以说这道题目需要一定思维,是因为我们需要对题目要求进行进一步的转化,也就是进一步利用前缀和,在前缀和上进行分析,从最终目标上看,任意区间为正即前缀和序列单调递增(单调不减不可以的,要严格单调递增),然后我们利用前缀和的性质来分析这个操作的实质,如下图所示
因此,我们对题意进行转化后就是对前缀和序列进行最少次数的两两元素位置交换使得最终的前缀和序列单调递增。
另外,还有一个小细节,也是我刚刚写题解时才发现的,之前看洛谷大佬题解时也没发现,就是这里题目说,也就是说我们进行操作时选定的区间和必须小于0的,重新注意到这条规则,心里猛地打怵了一下,因为我们把操作变成了交换前缀和序列中两个元素的位置会不会不符合这个要求呢?其实不会,因为我们交换的目的是使得前缀和序列单调递增且要满足交换次数最少的限制,所以我们在这个过程中只会把前面大的和后面小的进行交换,后面小的减去前面大的得到的区间和当然符合区间和为负的这一个条件。
关键步骤分析
int a[maxn],b[maxn],s[maxn];
int n;
int main(){
cin>>n;int mx;
for(int i=1;i<=n;i++){
cin>>a[i];
s[i]=s[i-1]+a[i];
b[i]=s[i];
mx=s[n];
}
sort(s+1,s+1+n);
if(s[1]<0||mx!=s[n]){
cout<<-1<<endl;
return 0;
}
for(int i=1;i<=n-1;i++){
if(s[i]==s[i+1]){
cout<<-1<<endl;
return 0;
}
}
int cnt=unique(s+1,s+1+n)-(s+1);
for(int i=1;i<=n;i++) b[i]=lower_bound(s+1,s+1+cnt,b[i])-s;
这里我们用到三个数组a,b,s,a用来存储原始数列同时s预处理获得最初的前缀和序列,b一开始复制一份一模一样的前缀和序列,a后面就没有用了,因为我们题意转换后已经用不着原始数列了,mx保存的是排序前的前缀和序列的最后一个元素(目的之后马上会看到),然后离散化的话首先是对序列进行排序,排序后如果最小的元素小于0可以直接输出-1结束程序了,因为这样原始序列的a1一个元素组成的区间和就不满足条件不为正,因为题目里说了r的范围小于n所以最后一个前缀和元素是不能交换的,所以它一开始就必须是最大的,排序后它依然要是最大的,不然怎么交换都得不到单调递增的结果。然后循环过程中判断有任何相邻前缀和元素相等也输出-1,因为这意味着做不到单调递增,只能做到单调不减。最后去重归位,b就是我们得到的离散化序列了,这是离散化的套路步骤,就不在这里过多赘述了,这篇博客主要是讲述一下离散化和其他知识点在题目中的灵活运用。这里进行离散化的主要目的我觉得是为了得到序列的相对大小关系,也就是说b表示当前位于i位置的元素在排序后应该排在的位置,也就是我们的目标位置,也就是说这里离散化是方便的利用相对大小关系得到了每个位置的元素它们的目标位置,有了这个依据,我们才能进行后面的交换。
for(int i=1;i<=n;i++) s[b[i]]=i;
// for(int i=1;i<=n;i++) cout<<b[i];cout<<endl;
// for(int i=1;i<=n;i++) cout<<s[i];cout<<endl;
int ans=0;
for(int i=1;i<=n;i++){
if(s[i]==i) continue;
else{
swap(b[s[i]],b[i]);
swap(s[b[s[i]]],s[i]);
// for(int i=1;i<=n;i++) cout<<b[i];cout<<endl;
// for(int i=1;i<=n;i++) cout<<s[i];cout<<endl;
ans++;
}
}
cout<<ans<<endl;
return 0;
然后我们又重新对s进行了赋值,可以看到我的注释里曾经输出了一下b和s,因为在题目给的测试样例里二者是一样的,一开始我一直不理解为什么还要有个s数组,我既然已经知道了目标b了,何不直接交换b[i],b[b[i]],然后我又自己举了个长一点的前缀和序列做例子发现,是我没有理解b和s的含义,题解 P1667 【数列】 - 洛谷专栏 (luogu.com.cn),在这个大佬的文章里,大佬说的贪心策略是“每个数都有一个自己的位置,那么我们就每当遇到一个位置上面的数不正确,就把应该在这个位置的数和当前这个位置的数互换。”,而swap(b[i],b[b[i]])的逻辑是“把当前位置的数和它应该在的位置进行互换”,这两句话不仔细一点真的容易混淆,而且二者效果是完全不一样的。
这里顺便把自己举的例子摆上来
我们需要明确b和s俩数组的含义
b的值现在表示的是现在在第i
位置的元素在排序后应该在的位置。
s指原数组中值为i
的元素现在在的位置。/原数组中应该排在i的元素现在在哪个位置
所以通过我们的交换,最终使得原数组中值为i即应该在i现在所在位置也在i即可。
swap(b[s[i]],b[i]);
swap(s[b[s[i]]],s[i]);
然后现在再来品一品这个贪心的交换代码,根据我草稿中的例子,以第一次循环为例
第一行:需要排在第一个位置的元素现在所在的位置和第一个位置的两个b数组位置的值进行交换,也就是符合我们贪心策略所说的应该排在当前位置的元素和当前元素进行交换,但在此之前我们要把指引我们去往目标的b数组先交换好
第二行:b[s[i]]现在就是b[i](上一步交换了),即第一个元素应该排在的那个位置上的当前是谁,我们就让它和应该排在第一位的元素进行交换。
补充一点新的理解:对于b数组,要想把应该位于第一的元素和当前元素交换位置,就是找到应该位于第一的元素和b[i]交换位置,所以要找到应该位于第一的元素现在在哪里,才能进行交换,所以是利用s[i]找到其目前的位置然后交换,我们需要利用s数组锁定应该处在i位置的元素现在在哪,所以要紧接着实时更新s数组,该处于1的元素现在真的在位置1了,所以是s[i]要换成1即现在应该处在位置1的元素就在位置1,那么具体谁和s[i]换呢,我们想想还有谁所处的位置发生了变化,那就是原来处在第1个位置的元素现在位置肯定变了,交换它们俩就行了,那么怎么表示原来处在第1个元素的位置呢,原来处在第1个位置的元素应该处在b[i],所以应该处在b[i]的元素原来的位置在1,即s[b[s[i]]](考虑到上一步交换,这里不应该是b[i])。上午发的这篇文章 ,下午进行了这点补充,因为期间一直在想更好的理解来解释这里的交换操作,这个交换操作是我看大佬题解时理解起来最吃力的地方。
#希望自己通过发博客可以加深对学习过程的理解,因为是学习过程的理解,所以很可能会存在错误和问题,欢迎大家批评指正,友好讨论,文章会实时更新,如果有大佬莅临指点那我自然更是开心啦#