程序设计作业week04

A - DDL 的恐惧

题目
ZJM 有 n 个作业,每个作业都有自己的 DDL,如果 ZJM 没有在 DDL 前做完这个作业,那么老师会扣掉这个作业的全部平时分。
所以 ZJM 想知道如何安排做作业的顺序,才能尽可能少扣一点分。
请你帮帮他吧!

输入
包含T个测试用例。输入的第一行是单个整数T,为测试用例的数量。
每个测试用例以一个正整数N开头(1<=N<=1000),表示作业的数量。
然后两行。第一行包含N个整数,表示DDL,下一行包含N个整数,表示扣的分。

输出
对于每个测试用例,您应该输出最小的总降低分数,每个测试用例一行。

样例

Sample Input

3
3
3 3 3
10 5 1
3
1 3 1
6 2 3
7
1 4 6 4 2 4 3
3 2 1 7 6 5 4

Sample Output

0
3
5

Hint

上方有三组样例。
对于第一组样例,有三个作业它们的DDL均为第三天,ZJM每天做一个正好在DDL前全部做完,所以没有扣分,输出0。
对于第二组样例,有三个作业,它们的DDL分别为第一天,第三天、第一天。ZJM在第一天做了第一个作业,第二天做了第二个作业,共扣了3分,输出3。

题解

题意理解:
一天只能做一个任务,当任务超过ddl未完成时会有相应的扣分。要使最后扣分扣得最少,我们就应该尽量避免分值大的任务丢分,所以在考虑选择任务时,扣分多的任务应该优先完成。(其实这跟赚分是一样的)

但并不是分值高的任务越早完成越好,每一个任务安排的时间会影响到其他任务的完成时间,所以还要考虑让影响降低到最小。

做法:

easy版
• 优先考虑价值高的任务,把n 个 deadline 根据分数从大到小排序,依次遍历
• 对于第i个 deadline,根据 ti 从后往前遍历,一旦找到空闲时段则安排为第 i 个 deadline。从ddl往前遍历可以保证第 i 个 deadline的安排时间对其他 deadline 的影响最小,因此结果更优
• 时间复杂度较高,为n方

hard版
• 从最晚ddl时间往前枚举每一天,给每一天安排任务
• 枚举到第 x 天时,将所有 ti = x 的 deadline 加入最大堆中
• 再从最大堆中选取一个分值最大的 deadline 安排在第 x 天
• 时间复杂度更小,为nlogn

这两种方法实现起来都不难,这里我用的是第二种方法。

代码解释

定义一个结构体ddl,成员有:ddl的时间,任务的分值,jd表示该任务是否完成,初始值设为0,还有任务的编号。重载<号让ddl在堆中按分值大小排列。

在输入ddl信息时,用maxt记下最晚的ddl的时间,在从后往前遍历日期时,从最后一个ddl时间开始–就行了。

从后往前给每天安排任务,每天遍历一遍ddl,找到ddl在当天的加入堆中。这样堆里都是在ddl前可以完成的任务,当堆不为空时,选择堆顶完成,标记任务的 jd = 1 然后pop。

最后统计所有ddl中有哪些任务未完成,累加扣分最后输出。

完整代码

#include <iostream>
#include <algorithm> 
#include <queue>
using namespace std;
int N;
struct	ddl
{
	int t, s, jd;
	int num;
	bool operator<(const ddl x) const
	{
		return this->s < x.s;
	}
}d[1010];
int main()
{
	cin>>N;
	for(int i=0;i<N;i++)
	{
		priority_queue<ddl> h;
		int n;
		cin>>n;
		int maxt = 0;
		for(int j=0;j<n;j++)
		{
			cin>>d[j].t;
			if(d[j].t > maxt)	maxt = d[j].t;
			d[j].num = j;
			d[j].jd = 0;
		}
		for(int j=0;j<n;j++)
			cin>>d[j].s;
		for(int k=maxt;k>0;k--)
		{
			for(int j=0;j<n;j++)
			{
				if(d[j].t == k)
				{
					h.push(d[j]);
				}
			}
			if(!h.empty())
			{
				ddl tp = h.top();
				d[tp.num].jd = 1;
				h.pop();
			}
		}
		int ans = 0;
		for(int j=0;j<n;j++)
		{
			if(d[j].jd == 0)	
			ans+=d[j].s;
		}
		cout<<ans<<endl;
	} 
	return 0;
}

总结
这是一个贪心问题,找到合理的贪心策略并保证他是最优解比较关键。这一题有两个小细节需要注意,一个是在弹出堆顶的时候,需要判断一下堆是否为空,因为不一定会有可执行任务在堆中,不然访问空会RE;另一个问题就是从什么时候开始往前遍历,一开始没有注意到这个问题就WA了。。。仔细读题qwq。

B - 四个数列

题目

ZJM 有四个数列 A,B,C,D,每个数列都有 n 个数字。ZJM 从每个数列中各取出一个数,他想知道有多少种方案使得 4 个数的和为 0。
当一个数列中有多个相同的数字的时候,把它们当做不同的数对待。
请你帮帮他吧!

Input

第一行:n(代表数列中数字的个数) (1≤n≤4000)
接下来的 n 行中,第 i 行有四个数字,分别表示数列 A,B,C,D 中的第 i 个数字(数字不超过 2 的 28 次方)

Output

输出不同组合的个数。

样例

Sample Input

6
-45 22 42 -16
-41 -27 56 30
-36 53 -37 77
-36 30 -75 -46
26 -38 -10 62
-32 -54 -6 45

Sample Output

5

题解

题意理解:

在四个数列中各选一个数字,要使选出来的四个数字之和恰好等于零,有相同数字算不同情况。

做法

看到题目的第一反应就是暴力搜,四层循环遍历四个数列,如果出现了和为0的情况,计数++。但是这样n的四次方,无疑时间复杂度太高了。

使用以下方法将n^4降到 n方:

• 分别用两重循环枚举AB数列两数之和、CD数列两数之和;
• 再用两层循环计算解的数量:第一层循环遍历AB的所有和,第二层循环,针对每一个AB的和去遍历CD的和,找到CD和中有多少个AB和的相反数,即四个数的和为0.
• 在查找相反数时,就使用二分查找的方法。

代码解释

sum1用来存AB数列选的两个数的和,sum2用来存CD数列的和。由于数列的大小是不超过4000,所以两两之和的数量不超过4000*4000.

在两两遍历完四个数列后,要对每一个sum1去sum2中寻找相反数。为了使用速度更快的二分查找,先用sort将数组排序。

标记查找范围的头和尾:st 和 ed,用mid表示二分点。用二分法找到sum2中最先是sum1相反数的下标,再继续从此处开始向后遍历,直到找完所有等于相反数的sum2.(因为sum2中可能不止一个相反数,又因为sum2是有序的,所以可能后面紧接着的一小段均满足条件,下面代码标有注释)

完整代码

#include <iostream>
#include <algorithm>
using namespace std;
int n;
int a[4010],b[4010],c[4010],d[4010];
int sum1[16000010];
int sum2[16000010];
int main()
{
	cin>>n;
	for(int i=0;i<n;i++)
		cin>>a[i]>>b[i]>>c[i]>>d[i];
	int kk = 0;	//记和的下标 
	for(int i=0;i<n;i++)
	{
		for(int j=0;j<n;j++)
		{
			sum1[kk]=a[i]+b[j];
			sum2[kk]=c[i]+d[j];
			kk++;
		}
	}
	sort(sum2, sum2 + kk);
	int mid;
	int ans = 0; 
	for(int i=0;i<kk;i++)
	{
		int st=0;
		int ed=kk-1;
		while(st < ed)      
		{
			mid = (st + ed)>>1;
			if(sum1[i] + sum2[mid] >= 0)	//数字偏大或者刚好 往前找 
				ed = mid;
			else	st = mid + 1;        
		}
		//前面判断时用的>=号,找到的是第一个满足条件的,后半区域可能还有=的情况 
		while(sum1[i] + sum2[st] == 0 && st < kk) 	
		{
			ans++;
			st++;            
		} 
	}
	cout<<ans<<endl;
	return 0; 
 }  

总结

用二分法得确保是有序的或者说单调的。
二分查找时,查找区域的头和尾的更新标记容易出错,需要仔细一点。

C - TT 的神秘礼物

题目

TT 是一位重度爱猫人士,每日沉溺于 B 站上的猫咪频道。
有一天,TT 的好友 ZJM 决定交给 TT 一个难题,如果 TT 能够解决这个难题,ZJM 就会买一只可爱猫咪送给 TT。

任务内容是,给定一个 N 个数的数组 cat[i],并用这个数组生成一个新数组 ans[i]。新数组定义为对于任意的 i, j 且 i != j,均有 ans[] = abs(cat[i] - cat[j])。试求出这个新数组的中位数,中位数即为排序之后 (len+1)/2 位置对应的数字,’/’ 为下取整。

TT 非常想得到那只可爱的猫咪,你能帮帮他吗?

Input

多组输入,每次输入一个 N,表示有 N 个数,之后输入一个长度为 N 的序列 cat, cat[i] <= 1e9 , 3 <= n <= 1e5

Output

输出新数组 ans 的中位数

样例

Sample Input

4
1 3 2 4
3
1 10 2

Sample Output

1
8

题解

题意理解:

将一个数列中的所有数字两两作差,取绝对值,构造了一个新的数组。要找出新构造数组的中位数是多少。

做法

暴力枚举

•枚举i, j将数列B计算出来,然后排序取出中位数;
•时间复杂度和空间复杂度均为n方,数据范围较大,难以接受。

二分答案

换一种角度思考,给出数列后,答案的范围是已知的,绝对值最小不会小于0,而最大值就是排序后的数列 a[n-1] - a[0]。既然范围已知,那我们看看答案在某种意义上,是否满足单调的二分条件。

先思考:如果给定一个数P,如何判断它是不是中位数?
计算P在数列中的名次即可。

•如果P从小到大排的名次比中位数小,说明什么? P比中位数要小
•如果P从小到大排的名次比中位数大,说明什么? P比中位数要大
•如果P从小到大排的名次等于中位数,说明什么? P就是中位数

结论:满足单调性,可以二分P!(源于课堂PPT)

这样的话,就确定了可以二分答案范围来逐渐确定答案。那么在确定待定“答案”P的时候,我们要计算名次判断P的大小是否真的处于中间位置。

那么如何计算名次呢?

•将X从小到大排列可以去绝对值。 那么计算Xj - Xi <= P的二元组对数即可。
•移项可得 Xj <= Xi + P, 枚举下标i然后计算满足条件的下标j的个数 也可以二分!

代码解释

输入数列后对数组进行排序,标记答案范围的头和尾:l = 0, r = a[n-1]-a[0],用mid记在该范围下答案的二分值,m是中位数该有的大小名次,等于绝对值元素数量加一除以2,是总体一半。

接下来对答案进行二分,更新范围,更新l、r、mid的值。前面说到在计算满足 Xj <= Xi + P 的下标j的个数时,可以使用二分的方法,这里我使用 lower_bound( ) 函数查找第一个大于或等于 Xi + mid 的数字,找到返回该数字的地址。(此处感谢大佬建议qwq)

补充:lower_bound( begin,end,num):从数组的begin位置到end-1位置二分查找第一个大于或等于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。
参考:lower_bound( ) 函数_博客

由于更新l值时为方便表示范围一直用的mid + 1,所以在最后输出时要-1.

完整代码

#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
int n, m, mid;
int main()
{
	while(scanf("%d", &n) != EOF)
	{
		int a[100100];
		for(int i=0;i<n;i++)
		{
			scanf("%d", &a[i]);
		}
		sort(a, a+n);
		int l = 0, r = a[n-1]-a[0];	//r为最大差
		m = ((n-1)*n/2 + 1) / 2;	//中位数应该的大小位置
		while(l<r)
		{
        	mid = (l+r) / 2;
        	int rk = 0;
    		for(int i=0;i<n;i++)
			{
        		rk += n - (lower_bound(a, a+n, a[i]+mid) - a);
   			}
        	if(rk > (n*(n-1)/2 - m)) 		l = mid + 1;
        	else 	r = mid;
        }
		printf("%d\n", l-1); 
	}
	return 0;
} 

总结

知道答案的范围,确定以某种形式单调,使用二分法确定真正的答案,在计算名次时第二次用到了二分。窝觉得这题挺难的,受益匪浅><

发布了10 篇原创文章 · 获赞 3 · 访问量 756

猜你喜欢

转载自blog.csdn.net/wakeupshely/article/details/104827103