火柴排队 题解
\(1.\) 题目关键点
这道题其实就是一道求 逆序对 的题
那为什么是求逆序对呢?
我们来分析一下题目:
题目要求交换完以后要 \(\sum\limits_{i=1}^n (a_i - b_i)^2\) 最小
拆分得
因为 \({a_i}^2 + {b_i}^2\) 是固定的,所以最小值就在 \(a_i b_i\) 上,即 \(a_i,b_i\) 越大,代数式的值就越小
那当 \(\sum\limits_{i=1}^n (a_i b_i)\) 最大时,便是本题最优解
那如何取最大值?
这里先给出一个策略(证明放在后面):
对于数列 \(p_1 \sim p_n,q_1 \sim q_n\),
或者上面公式中 \(p,q\) 按降序排列也行。
其实上面公式中所表示的含义就是 顺序之乘 \(\geq\) 乱序之乘
接下来证明一下 如果有人直接想看代码可以 跳过(bushi
\(1.\) 设升序数列 \(p_1 \sim p_n,q_1 \sim q_n\) 其中 \(0< p_1 < p_2,0< q_1 < q_2\)
\(2.\) 推论过程,先给出一个结论:
\[p_1 q_1 +p_2 q_2> p_1 q_2 +p_2 q_1 \]\(3.\) 证明这个结论:
\[\text{移项,得} \quad p_1 q_1 +p_2 q_2 - p_1 q_2 - p_2 q_1 >0 \\ \\ \]\[\text{合并同类项,得} \quad p_1(q_1 - q_2) +p_2(q_2 - q_1) >0 \\ \\ \]\[\text{转换,得} \quad p_1(q_1 - q_2) - p_2(q_1 - q_2) >0 \\ \\ \]\[\text{再合并同类项,得} \quad (p_1 - p_2)(q_1 - q_2)>0 \\ \\ \]\[\because p_1 < p_2 ,q_1 < q_2 \quad \therefore p_1-p_2<0 , q_1 - q_2<0 \\ \\ \]\[\therefore (p_1 - p_2)(q_1 - q_2)>0 \\ \\ \]\[\text{由} (p_1 - p_2)(q_1 - q_2)>0 \text{向上推,最终得出} \\ \\ \]\[p_1 q_1 +p_2 q_2> p_1 q_2 +p_2 q_1 \\ \\ \]因此该公式成立
\(4.\) 推广该结论到整个数列中,乱序之乘就相当于不断将交换顺序打乱的过程,最终符合该结论的标准,最终得出 顺序之乘 \(\geq\) 乱序之乘,证毕
那么,证明好这个结论,我们继续往下看
由题目要 \(\sum\limits_{i=1}^n (a_i b_i)\) 最大,我们由上面的公式推导可以想出:先给 \(a,b\) 分别从小到大排序,让 \(a\) 中第一小的数对 \(b\) 中第一小的数,让 \(a\) 中第二小的数对 \(b\) 中第二小的数……以此类推
那最小距离就出来了,可是我们怎么求出最小交换次数呢?
我们举一个例子 其实就是样例1:
4
2 3 1 4
3 2 1 4
我们由样列知,\(a\) 数组为 \(a = \{2,3,1,4 \}\),\(b\) 数组为 \(b = \{3,2,1,4 \}\)
对两个数组分别从小到大排序,得
信息 \(\backslash\) 数组 | \(a\) | \(b\) |
---|---|---|
排序过后的值 | \(\{1,2,3,4\}\) | \(\{1,2,3,4\}\) |
该数组中该值对应的原数组的编号 | \(\{3,1,2,4\}\) | \(\{3,2,1,4\}\) |
我们来依次处理一下:
首先看 \(a_1\) 的编号与 \(b_1\) 的编号,都是 \(3\),因此不用管
再来看一下 \(a_2\) 的编号与 \(b_2\) 的编号,\(a_2\) 编号是 \(1\),\(b_2\) 编号是 \(2\),是一个逆序对,不符合“相同排名一一对应”的原则
这里,产生了不符合原则的数对,所以我们可以另外定义一个数组 \(ma\),以进行演算。这也是本程序的核心
我们看一下 \(ma\) 如何工作的:
首先,设 \(ma_{b_i \text{的编号}} = a_i \text{的编号}\)
还是以上面的样例为例,操作是这样的:
- \(ma_{b_1 \text{的编号}} = a_1 \text{的编号} \rightarrow ma_3=3\)
- \(ma_{b_2 \text{的编号}} = a_2 \text{的编号} \rightarrow ma_2=1\)
- \(ma_{b_3 \text{的编号}} = a_3 \text{的编号} \rightarrow ma_1=2\)
- \(ma_{b_4 \text{的编号}} = a_4 \text{的编号} \rightarrow ma_4=4\)
- 所以 \(ma= \{2,1,3,4\}\),逆序对个数为 \(1\)(交换 \(ma_1\) 与 \(ma_2\))
看,这就是 排序 的魅力!
为什么上述操作能够实现?
因为产生了 逆序:只要原序列中的 \(a_i\) 与 \(b_i\) 的排名是一一对应的,那么排序以后两个数的 相对位置不变,因此不会产生逆序;而如果不是一一对应的,排完序两个数的 相对位置就会发生变化,因而产生逆序对
求逆序对有多种方法:冒泡、归并、树状数组等
这里由于 本蒟蒻太菜,只会冒泡和归并,冒泡又会超时,所以用归并写的
如果不知道归并或想用树状数组写可以自行 出门右转 百度
好了,那么思路就推导完了,接下来如果读者已经懂了可以自己尝试写一下
如果还想看代码,也可以往后面看(bushi
\(2.\) 代码实现
本部分将分步骤写成
\(2.1\) 定义与模板
这部分比较简单,代码里有注释
//#pragma GCC optimize(3)
//#pragma GCC optimize(2)
//上方是O(2)与O(3)的优化,如果超时可以尝试开一下(但是本程序不会)
//要注意的是,上方代码不可以再洛谷取消注释后提交,不然会CE(别问我怎么知道的)
#include<bits/stdc++.h>//万能头
using namespace std;//命名空间
const int mod=99999997;//按题目定义的模数
int n,ans=0;//ans指最少要交换几次
struct match{//定义结构体match(火柴)
int high,id;//high指火柴高度,id指火柴排序后在原来数列的编号
inline bool operator < (const match &temp)//重载运算符,不会的自己百度
const{return high<temp.high;}//按照高度从小到大排序
}a[100009],b[100009];
int ma[100009],temp[100009];//ma的定义在上文,temp是对ma归并排序时的临时数组
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0); //玄学的cin,cout优化
return 0;
}
\(2.2\) 预处理
预处理包含输入、离散化、排序、初始化好 \(ma\) 数组的值
//#pragma GCC optimize(3)
//#pragma GCC optimize(2)
#include<bits/stdc++.h>
using namespace std;
const int mod=99999997;
int n,ans=0;
struct match{
int high,id;
inline bool operator < (const match &temp)
const{return high<temp.high;}
}a[100009],b[100009];
int ma[100009],temp[100009];
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i].high,a[i].id=i;
for(int i=1;i<=n;i++) cin>>b[i].high,b[i].id=i;//输入,由于数据太大,需要离散化
sort(a+1,a+n+1),sort(b+1,b+n+1);//对火柴排序
for(int i=1;i<=n;i++) ma[b[i].id]=a[i].id;//ma的用法上问所示
return 0;
}
\(2.3\) 归并排序求逆序对及输出
归并排序……都写到 \(\text{蓝题}\) 就不用我讲了吧……
不过如果想知道归并排序可以看看 这些
只是模板罢了
//#pragma GCC optimize(3)
//#pragma GCC optimize(2)
#include<bits/stdc++.h>
using namespace std;
const int mod=99999997;
int n,ans=0;
struct match{
int high,id;
inline bool operator < (const match &temp)
const{return high<temp.high;}
}a[100009],b[100009];
int ma[100009],temp[100009];
inline void Sort(int l,int r)//归并排序模板,我就不多说了
{
if(l>=r) return;
int mid=(l+r)>>1;
Sort(l,mid),Sort(mid+1,r);
int i=l,j=mid+1,k=l;
while(i<=mid&&j<=r)
{
if(ma[i]<=ma[j]) temp[k++]=ma[i++];
else temp[k++]=ma[j++],ans+=(mid-i+1)%mod,ans%=mod;
}
while(i<=mid) temp[k++]=ma[i++];
while(j<=r) temp[k++]=ma[j++];
for(int point=l;point<=r;point++) ma[point]=temp[point];
return;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i].high,a[i].id=i;
for(int i=1;i<=n;i++) cin>>b[i].high,b[i].id=i;
sort(a+1,a+n+1),sort(b+1,b+n+1);
for(int i=1;i<=n;i++) ma[b[i].id]=a[i].id;
Sort(1,n);//对ma进行归并
cout<<ans;//输出,不需要我讲了吧
return 0;
}
\(2.4\) 最终无注释 高清 代码
这个我就不放在这里了吧,毕竟前面已经演示过很多次了
想知道的可以看 这里
\(3.\) 总结
知识点:不等式、归并求逆序对、离散化等
题目涉及知识点不难,只是要慢慢细心地推,发现求逆序对的 本质