吴永辉5-6 (归并排序,模拟优先队列,set,哈希存储)

Ultra-QuickSort(归并)

Description
n this problem, you have to analyze a particular sorting algorithm. The algorithm processes a sequence of n distinct integers by swapping two adjacent sequence elements until the sequence is sorted in ascending order. For the input sequence
9 1 0 5 4 ,
Ultra-QuickSort produces the output
0 1 4 5 9 .
Your task is to determine how many swap operations Ultra-QuickSort needs to perform in order to sort a given input sequence.

INPUT
The input contains several test cases. Every test case begins with a line that contains a single integer n<500,000 – the length of the input sequence. Each of the the following n lines contains a single integer 0≤a[i]≤999999999, the i-th input sequence element. Input is terminated by a sequence of length n=0. This sequence must not be processed.

OUTPUT
For every input sequence, your program prints a single line containing an integer number op, the minimum number of swap operations necessary to sort the given input sequence.
样例
输入
5
9
1
0
5
4
3
1
2
3
0
输出
6
0
题目大意
给你N个数字,在只能交换相邻两个数字的前提下,将这N个数字转化升序,求最小的交换次数!

方法
归并排序
:1.什么是归并排序
归并排序是一种“分治的思想”,即将一个大问题转化成若干个小问题,逐个解决。
2.理论实现:
假如有数字 5 4 3 2 1 0 我们要把这六个数字变成升序
我们可以先把 5 4 3 2 1 0分成 5 4 3 和 2 1 0两部分
然后再将5 4 3分成5 4和 3,2 1 0分成 2 1和0,再继续开始分。直到每个组都只有一个单个的数字,即5,4,3,2,1,0。然后开始逐个比较大小,交换顺序。5 4结合之后变成4 5,4 5再跟 0结合 变成 0 4 5,依次类推。。。。。
3.图像分析:
在这里插入图片描述
(这是我从一个大佬的博客上面嫖出来的图)
这个看起来会比较清晰:
我们可以自己找几个数字模拟一下,发现归并排序中
所交换的数字一定是两个相邻的数字,不可能出现不相邻还交换的情况!
具体实现
AC代码

#include<cstdio>
const int maxn=500000+5;
int num[maxn]; 
int tmp[maxn];
long long sum;
void merge(int low, int mid, int high) 
{  
    int i, j, k;
    i=k=low;
    j=mid+1;
    while(i<=mid && j<=high)
    {                            
        if(num[i]<num[j])
            tmp[k++]=num[i++];       
        else
        {
            sum+=j-k;                                               
            tmp[k++]=num[j++];
        }
    }
    while(i<=mid)
        tmp[k++]=num[i++];
    while(j<=high)//  
        tmp[k++]=num[j++];
    for(i=low; i<=high; i++)
        num[i]=tmp[i];
}
void mergeSort(int a, int b)
{
    int mid;
    if(a<b)
    { 
        mid=(a+b)/2;
        mergeSort(a, mid);
        mergeSort(mid+1, b);
        merge(a, mid, b);
    } 
}
int main() 
{
    int n;
    while(scanf("%d",&n)&&n)
    {
        int i;
        sum=0;
        for(i=0; i<n; i++)
            scanf("%d", &num[i]);
        mergeSort(0, n-1);
        printf("%lld\n", sum);
    }
    return 0;
}

对于变量的解释
由于作者比较菜,不能从思想上解决问题
只能从代码上面进行讲解
在这里我们的mergesort是一个递归的分割函数,
是用来将我们的数组进行拆分的工具
merge函数是用来进行具体数组转化并记录答案的函数
num数组是用来记录原数组的
tmp函数是用于num数组转化的一个媒介数组!
merge函数中的变量
i:当前排序区间中已经排好序部分的遍历计数器!
j:当前排序区间中未排好序部分的遍历计数器!
k:tmp数组的赋值计数器!
/
具体的实现过程(又臭又长)
我们用mergesort函数一步步的将原数组进行拆分。将一段连续的数组拆分左右两部分其中mid就是我们拆分左右的临界点。如果拆分之后 左端点=右端点,那么就说明拆分之后区间中的数字就一个,所以没必要进行排序,直接结束即可。如果 左端点<右端点,说明拆分后不止一个数字,我们要继续进行拆分。
由上面的图显示,我们要一步一步将所有数字都拆出来,再按原路返回一步一步的结合。所以这是一个递归类型的函数!先走到最里面,再一步一步的往外面爬
merge是一个结合的过程,假如我们已经拆到一个表的最低端之后,我们要开始进行结合并计数(记录答案)
但是我们的这个序列可能有某一段已经在之前的排序中已经排好了,原本就是升序队列了,所以前面这段不需要自我比较,我们只需要 先比较没排好序的区间,把后面没排好序的区间排好。
例如 4 5 3 2 1,假如 4 5在之前的递推中已经排好了,我们现在要做的就是 将 3 2 1排序成 1 2 3.再把 前面的4 5和后面的1 2 3逐个对比,把1 2 3一个一个的挪到前面去。
交换的过程:
我们在比较的过程中,只需要比较当前所需要排序区间中mid前和mid后的数字,用i来指示前,用j来指示后面,然后比较,如果前面<后面,说明不需要排序了,所以sum的值不变,但是我们为了后续可能的变动,要把每一步比较中较小的数字存起来(用k存),方便于后期的转换。如果前面>后面,此时不是升序,我们需要对我们的答案sum进行累加!sum+=j-k;因为他之前已经有k个比他还要小的数字了,并且他位于当前队列第J位,并且知道他是第k+1个小的数字,所以他应该向前移动 j-k位。
然后将小的数字继续的记录到tmp数组中。
最后将所有数组中的小数依次存起来
然后用tmp覆盖掉之前的数组,即使排序后的新数组!

Conformity(哈希打表)

Frosh commencing their studies at Waterloo have diverse interests, as evidenced by their desire to take various combinations of courses from among those available.

University administrators are uncomfortable with this situation, and therefore wish to offer a conformity prize to frosh who choose one of the most popular combinations of courses. How many frosh will win the prize?
INPUT
The input consists of several test cases followed by a line containing 0. Each test case begins with an integer 1≤n≤10000, the number of frosh. For each frosh, a line follows containing the course numbers of five distinct courses selected by the frosh. Each course number is an integer between 100 and 499.
OUTPUT
The popularity of a combination is the number of frosh selecting exactly the same combination of courses. A combination of courses is considered most popular if no other combination has higher popularity. For each line of input, you should output a single line giving the total number of students taking some combination of courses that is most popular.
样例
3
100 101 102 103 488
100 200 300 101 102
103 102 101 488 100
3
200 202 204 206 208
123 234 345 456 321
100 200 300 400 444
0

///

2
3
题目大意
每个学生可以选择5个自己喜欢的课程
一个组合被学生选择的次数成为该组合受欢迎的程度
求出受欢迎程度最大的课程组合数量

分析
一个学生要选择5门课程,此时我们需要对这5门课程所构成的组合赋一个初值0然后慢慢进行叠加。
但是怎么才能用一个什么东西 来表示这 5门课程结合后所代表的组合呢?
在这里,我们把每种组合构建一个“映射值”然后通过对映射进行操作从而表示对该组合进行操作
由于题意中说了组合的数量最多也就10000种,所以我们用5门课程所构成的15位数字%10007之后的结果代表我们的“映射值
但是有一个问题是,我们不能保证不出现两个15位数字%10007出现一个相同的“映射值
出现这种情况,我们就要判断一下,如果当前这个映射值已经被别的15位数字占用了,则他就不能再占用这个映射值,他应该遍历数组,直到找到一个新的别人没用过的位置作为他的“映射”值
这个时候这个映射值暂时没办法跟原来的15位数产生什么联系,在后续的遍历过程中没办法访问,所以我们开一个pos数组用来表示位置:即存储15位数字的映射//
有了映射值之后,只需要对映射值++即可标记欢迎度
最后遍历pop数组(存储受欢迎度的)即可

#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const long long MOD=100007; 
long long Hash[MOD],pos[MOD],pop[MOD],x[6];
int Getkey(long long num){ 
     int temp=num%MOD; 
     while(Hash[temp]!=-1&&Hash[temp]!=num)
         temp=(temp+1)%MOD;
     return temp;
}
int main()
{
     int n;
     while(scanf("%d",&n),n){
         long long ans=0;
         memset(pop,0,sizeof(pop));
         memset(Hash,-1,sizeof(Hash));
         for(int i=0;i<n;i++){
             for(int j=0;j<5;j++)
                 scanf("%lld",&x[j]);
             sort(x,x+5);
             long long temp=0;
             for(int j=0;j<5;j++)
                 temp=temp*1000+x[j];
             int ps=Getkey(temp);
             Hash[ps]=temp;
             pop[pos[i]=ps]++;
             ans=max(ans,pop[pos[i]=ps]);
         }
         int cnt=0;
         for(int j=0;j<n;j++){
             if(pop[pos[j]]==ans)
                 cnt++;
         }
         printf("%d\n",cnt);
     }
     return 0;
}

The Spot Game(set )

The game of Spot is played on an N×N board as shown below for N=4. During the game, alternate players may either place a black counter (spot) in an empty square or remove one from the board, thus producing a variety of patterns. If a board pattern (or its rotation by 90 degrees or 180 degrees) is repeated during a game, the player producing that pattern loses and the other player wins. The game terminates in a draw after 2N moves if no duplicate pattern is produced before then.

Consider the following patterns:

在这里插入图片描述
If the first pattern had been produced earlier, then any of the following three patterns (plus one other not shown) would terminate the game, whereas the last one would not.
INPUT
Input will consist of a series of games, each consisting of the size of the board, N (2≤N≤50) followed, on separate lines, by 2N moves, whether they are all necessary or not. Each move will consist of the coordinates of a square (integers in the range 1…N) followed by a blank and a character +' or-’ indicating the addition or removal of a spot respectively. You may assume that all moves are legal, that is there will never be an attempt to place a spot on an occupied square, nor to remove a non-existent spot. Input will be terminated by a zero (0).
OUTPUT
Output will consist of one line for each game indicating which player won and on which move, or that the game ended in a draw.
样例
2
1 1 +
2 2 +
2 2 -
1 2 +
2
1 1 +
2 2 +
1 2 +
2 2 -
0
//
Player 2 wins on move 3
Draw
题目大意
两个人轮流在棋盘上放置棋子或者拿走棋子,如果有一步操作导致棋盘上出现了一次重复的棋子图案(包括旋转后的图案),则游戏结束,该玩家输掉游戏!
题目分析

我们怎么样才能知道这个图案是否出现过呢,我们有什么办法能对一个图案进行标记表示他曾经出现过?

我们这边引入一个 C++中 STL中的set
set:set队列中每个元素仅能出现一次
并且一旦赋值之后就没办法改变
我们可以在某些去重题目中使用set达到元素不重复的目的!
我们把棋盘构成的二维数组想象成一个字符串
然后用set存储起来。如果某一次存储的过程中发现这个元素之前有过,则说明重复

由于某一个棋盘当前的状态,其实有向左旋转90度,向右旋转90度,旋转180度(左旋180和右旋180是一样的),和原图四种情况
开一个二维数组s,用s[4][n*n],S[0]表示原图,s[1],[2]表示左旋90和右旋90,s[3]表示旋转180.然后我们不断的把
s[0-3]放入set队列,其实就是把每次操作之后的四种图放入了队列。
这里的第一个难点
就是旋转:
//这是在首元素是下标0的前提下的旋转
原图:x * n+y;
右旋90:y * n+(n-x-1);
左旋90;(n-y-1) * n+x;
180: (n-x -1) * n + (n-y-1);

第二个难点
set函数的用法:
set.insert()插入一个元素
set.end() end() 返回set容器的最后一个元素(end()返回的是最后一个元素的下一个元素的索引,不是最后一个元素)
c++ stl容器set成员函数:end()–返回指向最后一个元素下一个位置的迭代器
(查找新的空位置)
set.find():c++ stl容器set成员函数:find()–返回一个指向被查找到元素的迭代器 (查找元素位置)

#include<bits/stdc++.h>
using namespace std;
int main() {
	int n;
	while (scanf("%d", &n)&&n!=0) {
		set<string>data;
		int x, y, flag = 0;
		char c, s[4][2600] = {0};
		for (int i = 0; i < 4; i++)
			for (int j = 0; j < n * n; j++)
				s[i][j] = '0';//
		for (int i = 1; i <= 2 * n; i++) {
			scanf("%d%d %c", &x, &y, &c);
			int spot = (c == '+' ? '1' : '0');
			x--, y--;
			s[0][x * n + y] = spot;
			s[1][y * n + (n - x - 1)] = spot;
			s[2][(n - y - 1) * n + x] = spot;
			s[3][(n - x - 1) * n + (n - y - 1)] = spot;
			if (data.find(s[0])!=data.end()&&flag==0)
				flag=i;
			for (int i = 0; i < 4; i++) 
				data.insert(s[i]);
		} 
		if (flag!=0)
			printf("Player %d wins on move %d\n", flag % 2 + 1, flag);
		else
			printf("Draw\n");	
	}
	return 0;
}

Sort(数组模拟优先队列)

Recently, Bob has just learnt a naive sorting algorithm: merge sort. Now, Bob receives a task from Alice.
Alice will give Bob N sorted sequences, and the i-th sequence includes ai elements. Bob need to merge all of these sequences. He can write a program, which can merge no more than k sequences in one time. The cost of a merging operation is the sum of the length of these sequences. Unfortunately, Alice allows this program to use no more than T cost. So Bob wants to know the smallest k to make the program complete in time.
INPUT
The first line of input contains an integer t0, the number of test cases. t0 test cases follow.
For each test case, the first line consists two integers N (2≤N≤100000) and T (∑Ni=1ai<T<231).
In the next line there are N integers a1,a2,a3,…,aN(∀i,0≤ai≤1000).
OUTPUT
For each test cases, output the smallest k.
样例
1
5 25
1 2 3 4 5

///
3
题目大意
给出一个数组,每次可以结合数组中K个数字,代价是这K个数字的数字和,问在代价不超过T的前提下,把所有数字结合成1个数字所需的K的最小值是多少?
题目分析
我们想求出最小的代价,必须每次加当前数组中最小的k个值才能实现代价最小的目的!
我最开始的思路是想利用优先队列,遍历K的值
每次去q.top k次,相加,再将得到的和放回队列中去。
但是有一个致命的问题,由于需要对K枚举,这种方式只能对循环的第一次管用,一旦到了第二次,优先队列很难通过简单的代码复原,就达不到遍历K的效果。

所以这里我们用两个数组完成优先队列的效果!

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+10;
int n,m;
int a[maxn];
int b[maxn];
int judge(int k) 
{   
    int sum=0;
    int t=n%(k-1);
	int num=n/(k-1);
	int f1,f2,r1,r2;
	r1=n,f1=t+1;
	f2=1,r2=1;
	for(int i=1;i<=t;i++)
	{
		sum+=a[i];
	 } 
	int zong=sum;
	b[r2]=sum;
    while(num--)
    {
    	int sum=0;
		for(int i=1;i<=k;i++)
		{
			if(f1>r1)
			{
				sum+=b[f2];
				f2++;
			}
			else if(f2>r2)
			{
				sum+=a[f1];
				f1++;
			}
			else 
			{
				if(a[f1]<b[f2])
				{
					sum+=a[f1];
					f1++;
				}
				else
				{
					sum+=b[f2];
					f2++;
				}
	
			}	
		 } 
    	zong+=sum;
    	if(zong>m)
    	{
    		return 0;
		}
    	r2++;
    	b[r2]=sum;
	}
	return 1;
}
int main()
{
	int num;
	cin>>num;
	while(num--)
	{
		cin>>n>>m;
		for(int i=1;i<=n;i++)
        {
        cin>>a[i];
     	}
     	sort(a+1,a+1+n);
		int l=2; //避免出现mid=1的情况! 
		int r=n;
		int mid;
		int ans;
		while(l<=r)
		{
			mid=(r+l)/2;
			if(judge(mid)==1)
			{
				r=mid-1;
				ans=mid;
			}
			else
			l=mid+1;
		}
		printf("%d\n",ans);
	}
	return 0;
}

1.二分答案:二分可以大大降低复杂度,但是要注意本题中我们的左边界L是从2开始的(至于为什么后面再讲)
2.我们设置一个A数组,一个B数组,A数组用来存我们的原数组,B数组用来暂时存储K个数的和!
3.每次结合K个数字,说明每次会消失K个数字,然后生成1新的数字,这就说明数字的减少量每次是(K-1)个,所以一个数组结合 N/(K-1)次才能结合完,所以我们用num=n/(k-1)表示我们结合数字的次数。所以为了避免k-1=0的情况,我们的二分范围L值发生变化!
4.我们每次结合K个数字,然后把K个数字的和放在新数组B中,我们再想加的数字的时候,就要进行一次判断。判断当前遍历点上的数组A和数组B上数字的大小(因为我们贪心的前提是加小数字)
5.如果一个数组的数字个数达不到k-1,则我们用上述算法算出来的结合次数num就是0,没法进行下去,所以我们需要另外算一个值 t=n%(k-1) ,进行一个特判处理,避免出现 数字不足K-1个的情况!
下面着重讨论一下优先队列的实现过程:
f1表示数组A的遍历计数器
f2表示数组B的遍历计数器
r1表述原数组A的元素总数
r2是数组B的赋值计数器,也是当前B数组的元素个数

分三种情况讨论
1.A数组遍历空了,只能加B数组中的值(f1>r1)
2.B数组遍历空了,只能加A数组中的值(f2>r2)
3.两个都不空,比较a[f1]和b[f2]选当前最小的!
4.把K次加的值通过计数器r2赋到数组B中!
5.把每次求得的代价相加,然后跟代价上限做比较,如果满足题意要求,二分总和即为答案!

2021/4/20.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值