多级排序
莫斯科正在举办一个大型国际会议,有n个来自不同国家的科学家参会。
每个科学家都只懂得一种语言。
为了方便起见,我们把世界上的所有语言用1到109之间的整数编号。
在会议结束后,所有的科学家决定一起去看场电影放松一下。
他们去的电影院里一共有m部电影正在上映,每部电影的语音和字幕都采用不同的语言。
对于观影的科学家来说,如果能听懂电影的语音,他就会很开心;如果能看懂字幕,他就会比较开心;如果全都不懂,他就会不开心。
现在科学家们决定大家看同一场电影。
请你帮忙选择一部电影,可以让观影很开心的人最多。
如果有多部电影满足条件,则在这些电影中挑选观影比较开心的人最多的那一部。
根据题意,我们可以先把科学家的语言统计成哈希表,再根据每个电影的语音和字幕统计听得懂和看得懂的人数,将每个电影保存成一个结构体,对这若干个结构体进行排序。
#include <iostream>
#include <unordered_map>
#include <algorithm>
using namespace std;
//a和b记录电影听得懂和看得懂的人数,c记录电影的序号,最终我们要输出电影序号。
struct node{
int a, b, c;
} ans[200010];
//m代表电影数,n代表科学家数量,index作为哈希表语言key。
int m, n, index;
unordered_map<int, int> num, sum;
// 结构体排序函数
bool cmp(node a, node b){
if(a.a==b.a) return a.b > b.b;
return a.a > b.a;
}
int main(){
//统计科学家的语言,形成哈希表sum
cin >> n;
for(int i=0; i<n; i++){
cin >> index;
sum[index]++;
}
//记录每部电影听得懂和看得懂的人数
cin >> m;
for(int i=0; i<m; i++){
cin >> index;
ans[i].a=sum[index];
ans[i].c=i;
}
for(int i=0; i<m; i++){
cin >> index;
ans[i].b=sum[index];
}
//对结构体进行排序
sort(ans, ans+m, cmp);
//输出排序后的首元素,+1是因为我们从0开始记录的,需要处理边界
cout << ans[0].c + 1;
return 0;
}
中位数问题
在一条数轴上有 N 家商店,它们的坐标分别为 A1~AN。
现在需要在数轴上建立一家货仓,每天清晨,从货仓到每家商店都要运送一车商品。
为了提高效率,求把货仓建在何处,可以使得货仓到每家商店的距离之和最小。
我们假设把货仓建在N家商店的中位数位置可以使得每家商店到货仓的距离之和最小,即货仓两侧的商店数量应一样多。如果不一样多,当货仓向中位数移动的时候,就会存在商店多的一侧距离之和减少,商店少的一侧距离之和增加,总体就会减小,因此可知,将货仓建在中位数是合理的。
#include <iostream>
#include <vector>
#include <limits.h>
#include <algorithm>
using namespace std;
int main(){
//记录n家商店的位置,形成数组
int n;
cin >> n;
vector<int> a(n);
for(int i=0; i<n; i++) cin >> a[i];
sort(a.begin(), a.end());
//选取中位数,记录距离总和
int mid = n >> 1;
int sum;
for(int i=0; i<n; i++){
sum += abs(a[i]-a[mid]);
}
cout << (int)sum;
return 0;
}
动态中位数:依次读入一个整数序列,每当已经读入的整数个数为奇数时,输出已读入的整数构成的序列的中位数。
该题我们可以维护两个堆,分别是大顶堆和小顶堆,其中大顶堆的堆顶就是我们当前的中位数,对于新输入的数,我们采取如下步骤
- 加上新元素,当前序列总数是偶数,那么与大顶堆堆顶比较,
(1)如果大于大顶堆堆顶,那么插入到小顶堆中
(2)如果小于大顶堆堆顶,那么取出大顶堆堆顶放入到小顶堆中,将新元素放入大顶堆。 - 加上新元素,当前序列总数是奇数,那么与小顶堆堆顶比较,
(1)如果小于小顶堆堆顶,那么插入到大顶堆中
(2)如果大于小顶堆堆顶,那么取出小顶堆堆顶放入到大顶堆中,将新元素放入小顶堆中。
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
vector<int> solve(int num){
int m;
//结果中位数数组
vector<int> ans;
//small小顶堆,big大顶堆
priority_queue<int, vector<int>, greater<int>> small;
priority_queue<int, vector<int>, less<int>> big;
//判断奇偶,
for(int j=1; j<=num; j++){
cin >> m;
if(j % 2){
if(!big.size() || m < small.top()) big.push(m);
else{
big.push(small.top());
small.pop();
small.push(m);
}
ans.push_back(big.top());
}
else{
if(m < big.top()){
small.push(big.top());
big.pop();
big.push(m);
}
else small.push(m);
}
}
return ans;
}
int main(){
//n表示输入组数,index是编号,num是序列长度
int n, index, num;
cin >> n;
for(int i=0; i<n; i++){
cin >> index >> num;
vector<int> res = solve(num);
//输出编号,以及中位数个数
cout << index << " " << (num + 1 >> 1) << endl;
//输出中位数
for(int j=1; j<=res.size(); j++){
cout << res[j-1] << " ";
if(!(j % 10) || j==res.size()) cout << endl;
}
}
return 0;
}
均分纸牌问题
n个人坐成一排,手里总共有m张牌,现在每次操作可以使得相邻两人之间传递一张牌,问至少需要多少次操作可以使得n个人手里的纸牌数相同。
我们可以从边界开始考虑,如果当前排在第一位的人手里牌数a1少于m/n,那么他必将从第二个人手中抽取a1-m/n张牌,使得自己手里的牌数达到均值。这时,我们只需考虑a2~an需要多少次操作才能使所有人手里的纸牌数相同,因此该题可以转化为贪心问题。
注:若第一位人的手里牌数多于m/n,那么他必将多余的牌传递给第二个人才能达到要求。若在计算过程中某人手里的牌数是负数也没有关系,这说明他和之前所有人手里的牌数是不够均分到m/n的,在实际过程中,我们会先分给他足够的牌数,传递给之前的人。
所以这道题就变成里求解
S
u
m
=
∑
i
=
1
n
∣
S
i
∣
Sum = \sum_{i=1}^{n}|S_{i}|
Sum=∑i=1n∣Si∣,其中
S
i
=
∑
j
=
1
i
(
A
j
−
m
/
n
)
S_{i}=\sum_{j=1}^{i}(A_{j}-m/n)
Si=∑j=1i(Aj−m/n),表示第i点需要和下一点传递的纸牌数
现在我们将难度提升一下,若这n个人做成一圈,那么问至少需要多少次操作可以使得n个人手里的纸牌数相同。
首先,给出一个结论,对于最少操作次数必然会有两人之间没有纸牌传递。
该结论的证明,我…不会…,以下给出个人简陋不严谨的思路。
对于该图我们可以简化成相邻两点,实线代表他们之间的传递,虚线代表其余点的传递流总和,蓝色代表纸牌走向。
因此我们可以将问题分成两种情况考虑,对于左边的情况,必然可以去掉一个相邻点之间的纸牌传递,使得纸牌传递不会存在循环流,减少传递次数。
而对于右边的情况,可以知道,右边红点手里的牌数是少于平均值的,那么我们可以让离其最近的一段连续区域(纸牌平均数多于总的平均数)来多给它传递,然后去掉离该点远的传递流,这样可以减少总的传递次数。(以后有更好的证明再来补…)
至此,我们可以将环形纸牌问题转化成普通纸牌问题,只需考虑在哪里将环形剪断,所得到的传递次数最少。我们假设,要在k和k+1之间将环剪断,那么,对于每个点所需的传递次数,表示如下:
k
+
1
:
S
k
+
1
−
S
k
k+1: S_{k+1}-S_{k}
k+1:Sk+1−Sk
k
+
2
:
S
k
+
2
−
S
k
k+2: S_{k+2}-S_{k}
k+2:Sk+2−Sk
n : S n − S k n: S_{n}-S_{k} n:Sn−Sk 1 : S 1 − S k + S n 1: S_{1}-S_{k}+S_{n} 1:S1−Sk+Sn
k : S k − S k + S n k: S_{k}-S_{k}+S_{n} k:Sk−Sk+Sn
其中 S i S_{i} Si和均分纸牌相同, S n = 0 S_{n}=0 Sn=0,所以公式可以简化成求解使总和最小的k,即 a r g m i n k ∑ S i − S k argmin_{k}\sum S_{i}-S_{k} argmink∑Si−Sk。至此,我们就可以将问题转化为货仓选址问题,求出中位数,就是最优解,我们来看一个例题。
七夕节因牛郎织女的传说而被扣上了「情人节」的帽子。
于是TYVJ今年举办了一次线下七夕祭。
Vani同学今年成功邀请到了cl同学陪他来共度七夕,于是他们决定去TYVJ七夕祭游玩。
TYVJ七夕祭和11区的夏祭的形式很像。
矩形的祭典会场由N排M列共计N×M个摊点组成。
虽然摊点种类繁多,不过cl只对其中的一部分摊点感兴趣,比如章鱼烧、苹果糖、棉花糖、射的屋……什么的。
Vani预先联系了七夕祭的负责人zhq,希望能够通过恰当地布置会场,使得各行中cl感兴趣的摊点数一样多,并且各列中cl感兴趣的摊点数也一样多。
不过zhq告诉Vani,摊点已经随意布置完毕了,如果想满足cl的要求,唯一的调整方式就是交换两个相邻的摊点。
两个摊点相邻,当且仅当他们处在同一行或者同一列的相邻位置上。
由于zhq率领的TYVJ开发小组成功地扭曲了空间,每一行或每一列的第一个位置和最后一个位置也算作相邻。
现在Vani想知道他的两个要求最多能满足多少个。
在此前提下,至少需要交换多少次摊点。
因为只能横纵交换,所以列交换不会影响最终行内感兴趣的摊点数,我们可以把问题拆分成两个环形均分纸牌问题,分别求解行列。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cstdlib>
using namespace std;
//最大横纵摊位数
const int u=100010;
long long b[u],c[u],f[u];
long long n,m,t,i,j,x,y;
long long calc(long long a[u],int n)
{
long long ans=0; int i;
for(i=1;i<=n;i++)
{
//形成S, 见均分纸牌公式
a[i]-=a[0]/n;
f[i]=f[i-1]+a[i];
}
//对S排序,选取中位数,求出结果
sort(f+1,f+n+1);
for(i=1;i<=n;i++) ans+=abs(f[i]-f[n+1>>1]);
return ans;
}
int main()
{
//n表示行数,m表示列数,t代表测试组数
cin>>n>>m>>t;
//对于每个感兴趣的摊位在行列处+1
for(i=1;i<=t;i++)
{
scanf("%d%d",&x,&y);
b[x]++,c[y]++;
}
//首位表示感兴趣摊位数总和
for(i=1;i<=n;i++) b[0]+=b[i];
for(i=1;i<=m;i++) c[0]+=c[i];
//判断感兴趣摊位数是否可以均分到行、列
if(b[0]%n==0&&c[0]%m==0)
printf("both %lld\n",calc(b,n)+calc(c,m));
else if(b[0]%n==0)
printf("row %lld\n",calc(b,n));
else if(c[0]%m==0)
printf("column %lld\n",calc(c,m));
else puts("impossible");
return 0;
}
逆序对问题
在这个问题中,您必须分析特定的排序算法----超快速排序。
该算法通过交换两个相邻的序列元素来处理n个不同整数的序列,直到序列按升序排序。
对于输入序列9 1 0 5 4,超快速排序生成输出0 1 4 5 9。
您的任务是确定超快速排序需要执行多少交换操作才能对给定的输入序列进行排序。
该问题就是需要计算序列中有多少对逆序对,我们要把所有逆序对全部交换,所以问题也可以改成冒泡排序需要多少次操作。
对于计算逆序对,常用的方法就是归并排序,朴素算法需要
O
(
n
)
O(n)
O(n)复杂度,而采用归并,可以降低为
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)。我们的关键就在于在归并的时候,采用双指针计算逆序对。如果前面指针所指的数要大于后面指针的数,这就说明前面序列的剩余元素都要大于后面指针所指元素,通过这种方式确定序列中的逆序对。用图来表示:
代码如下:
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
typedef long long ll;
//因为逆序对数有可能多余10的10次幂,我们定义成long long类型
ll res;
//归并排序
void merge(vector<ll>& nums, ll l, ll r){
if(l>=r) return;
ll mid = l + r >> 1;
merge(nums, l, mid);
merge(nums, mid + 1, r);
//fp是前面指针,从l开始,bp是后面指针,从mid+1开始
vector<ll> temp;
ll fp = l, bp = mid + 1;
for(ll i=l; i<=r; i++){
if(bp > r || fp <= mid && nums[fp] < nums[bp]) temp.push_back(nums[fp++]);
else{
//如果nums[fp]>nums[bp],我们要统计一下前面指针剩余元素个数
res += mid + 1 - min(mid + 1, fp);
temp.push_back(nums[bp++]);
}
}
ll index=0;
for(ll i=l; i<=r; i++) nums[i] = temp[index++];
}
void solve(int n){
ll num;
vector<ll> nums;
for(ll i=0; i<n; i++){
//输入序列
cin >> num;
nums.push_back(num);
}
merge(nums, 0, nums.size()-1);
}
int main(){
//n表示序列长度,当n为0则结束输入
ll n;
while(1){
cin >> n;
if(!n) break;
res = 0;
solve(n);
cout << res << endl;
}
return 0;
}
你一定玩过八数码游戏,它实际上是在一个3×3的网格中进行的,1个空格和1~8这8个数字恰好不重不漏地分布在这3×3的网格中。
例如:
5 2 8
1 3 _
4 6 7
在游戏过程中,可以把空格与其上、下、左、右四个方向之一的数字交换(如果存在)。
例如在上例中,空格可与左、上、下面的数字交换,分别变成:
左:
5 2 8
1 _ 3
4 6 7
上:
5 2 _
1 3 8
4 6 7
下:
5 2 8
1 3 7
4 6 _
奇数码游戏是它的一个扩展,在一个n×n的网格中进行,其中n为奇数,1个空格和1~n2−1这n2−1个数恰好不重不漏地分布在n×n的网格中。
空格移动的规则与八数码游戏相同,实际上,八数码就是一个n=3的奇数码游戏。
现在给定两个奇数码游戏的局面,请判断是否存在一种移动空格的方式,使得其中一个局面可以变化到另一个局面。
对于二维数组我们通常将其转化成一维数组,如同环我们通常转化成链
可以发现,当__在同一行内移动的时候不会改变整个数组的逆序对个数。
而当__在列内交换的时候会改变整个数组的逆序对个数。而该题的n为奇数,可以得出列交换并不会改变数组中逆序对个数的奇偶性。
因此可以说逆序对个数奇偶性不同的奇数码不能相互转换。而对于奇偶性相同的奇数码则可以相互转换(怎么证明我也不知道,知道了再来补上吧…)。
#include <iostream>
using namespace std;
//res记录逆序对个数,nums1记录第一个网格,nums2记录第二个网格,temp是临时数组
long long res;
int nums1[250010], nums2[250010], temp[250010];
//归并排序 同上题
void merge(int nums[], int l, int r){
if(l >= r) return;
int mid = l + r >> 1;
merge(nums, l, mid);
merge(nums, mid+1, r);
int fp = l, bp = mid + 1;
for(int i=l; i<=r; i++){
if(fp > mid || bp <= r && nums[fp] > nums[bp]){
res += mid + 1 - fp;
temp[i] = nums[bp++];
}
else temp[i] = nums[fp++];
}
for(int i=l; i<=r; i++) nums[i]=temp[i];
}
bool solve(int n){
//输入数据0表示下划线,我们将输入数据保存成一维数组
int num, i = 0;
while(i != n*n-1){
cin >> num;
if(!num) continue;
else nums1[i++] = num;
}
i=0;
while(i != n*n-1){
cin >> num;
if(!num) continue;
else nums2[i++] = num;
}
res=0;
//分别计算逆序对个数
merge(nums1, 0, n*n-2);
long long ans = res;
res=0;
merge(nums2, 0, n*n-2);
//返回两个数组逆序对个数奇偶性是否相同
return res%2 == ans%2;
}
int main(){
int n;
while(cin >> n){
if(solve(n)) cout << "TAK";
else cout << "NIE";
cout << endl;
}
return 0;
}