算法-程序设计课week4-二分与贪心-作业

A - DDL 的恐惧

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

Input

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

Output

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

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。

思路

我一开始是让分数最高的最先做,后来转念一想,如果出现这种情况怎么办:

3分任务第一天截止,7分任务第二天截止。只看分数的贪心会丢掉3分。

最后我终于想明白了,可以让分数高的任务最先挑选临近ddl的日子完成,这样既能保障高分任务必定做完,又能保证让低分任务也有充足的时间完成。

因此,这道题的贪心思路为:

  1. 优先做分值最高的,保障得分。
  2. 每个任务优先选择临近ddl的日期,以便尽可能为其他任务腾出空间。
    只需双层循环就可以完成核心任务。

有个小问题需要注意:
ddl是正常天数,数组下标+1才是正常天数。

心得
  1. 从题目可得,越是重要的任务就越应该在靠近ddl的时候做(笑)。
  2. 这个题目对贪心的应用比较经典,学习记录之。
  3. 平台测试样例直接粘贴到控制台发生格式错误,粘贴到文本文档则不会。
  4. 贪心法需要证明,但我不太会,这里贴一个别人的证明:

    贪心法的证明如下:
    假设A(a1,a2,a3,…an)为贪心解,O(o1,o2,o3,…on)为最优解,且假设oi为A和O第一个相同的分量(即aj=oj,1<=j<i,在之前A和O的分量都相同)那么,由贪心策略可知,point(ai)>=point(oi)。那么我们将oi替换为ai的话,结果不会变差。这证明了贪心法的正确性。
    https://blog.csdn.net/qq_43715114/article/details/104851430

#include<iostream>
#include<algorithm>
#define _for(i,s,e) for(int i = s;i<e;i++)
using namespace std;

struct task {
	int ddl;
	int score;
	bool sign;
	bool operator<(const task &t)const {
		return this->score>t.score;
	}
};
task ts[1005];
int day[1005];
int main() {
	int t,n;
	scanf("%d",&t);
	while(t--) {

		scanf("%d",&n);

		_for(i,0,n) {
			scanf("%d",&(ts[i].ddl));
			ts[i].sign=false;
			day[i]=false;
		}
		for(int i=0; i<n; i++)scanf("%d",&(ts[i].score));

		sort(ts,ts+n);//排序,优先选择高分值


		//对于i任务
		_for(i,0,n) {
			//每个任务都应当尽可能安排在靠近ddl的地方,以为其他任务腾出空间
			//分值高的任务应当优先保证有位置
			for(int d=n-1; d>=0; d--) {
				if((ts[i].ddl>d)&&day[d]==false) {
					ts[i].sign=day[d]=true;
					printf("work%d,day%d  ",ts[i].score,d);
					break;
				}
			}
		}

		int score=0;
		_for(i,0,n) {
			if(ts[i].sign==false)score+=ts[i].score;
		}
		printf("%d\n",score);
	}
	return 0;
}

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
Hint

样例解释: (-45, -27, 42, 30), (26, 30, -10, -46), (-32, 22, 56, -46),(-32, 30, -75, 77), (-32, -54, 56, 30).

思路

这个问题的核心在于如何优化时间复杂度

暴力

第一反应是暴力枚举,然而这样复杂度n4肯定是不行的;

四个数组二分

既然本周主题是二分与贪心,如果用二分呢?二分枚举n2 *n2=n4也不行

哈希表

这里影响复杂度的就是 * ,如何把 * 变成 + ?哈希表啊!反正都是存4000*4000个数字,为何不用哈希表代替数组?用了哈希表,复杂度n2
#include<map> 超时, unordered_map 编译器不支持,尝试tr1失败,上古编译器get。
回归C,使用数组作为哈希表 >> 段错误 MLE
使用multiset 当哈希表 >> TLE

二分查找

使用排序找区间的方法,一开始使用find函数,TLE。
换成自己写的二分查找终于AC了。

总结一波:
暴力枚举 TLE > 哈希表 CE > 数组哈希表 MLE > multiset .count TLE > find函数TLE > 自己写查找函数AC

心得
  1. 这道题尝试了很多方法,按理说哈希表是可以的,但数OJ版本太低没有哈希表,自己写又有点麻烦emmm。
  2. find函数性能不如自己写二分查找。
代码
#include<set>
#include<cstdio>
#include<algorithm>
#define _for(i,s,e) for(int i = s;i<e;i++)
using namespace std;


int a[4005],b[4005],c[4005],d[4005];
int sumab[16000005];
int sumcd[16000005];

int main() {
	int n;

	scanf("%d",&n);

	_for(i,0,n) {
		scanf("%d %d %d %d",a+i,b+i,c+i,d+i);
	}



	int t=0;
	_for(i,0,n) {
		_for(j,0,n) {
			sumab[t]=a[i]+b[j];
			sumcd[t]=c[i]+d[j];
			++t;
		}
	}

	sort(sumcd,sumcd+t);

	int total=0;
	for(int i=0; i<t; i++) {

		//¿ìËÙ²éÕÒ
		//int pos = find(sumcd+0,sumcd+t,0-sumab[i])-sumcd;//Õâ¸ö³¬Ê±
		int start=0;
		int end=t-1;
		int mid;
		while(start < end) {
			mid = (start + end)>>1;
			if(sumab[i] + sumcd[mid] >= 0) {
				end = mid;
			}else {
				start = mid + 1;
			}
		}
		int pos = start;
		while(sumab[i] + sumcd[pos] == 0 && pos < t) {
			total++;
			pos++;
		}
	}
	printf("%d",total);
	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]),1 <= i < j <= N。试求出这个新数组的中位数,中位数即为排序之后 (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
思路

简述该题目,我们的目标是求cat数组各元素之间距离的中位数。
暴力肯定白给。
二分需要单调,所以我们对输入进行排序。

下面分析求中位数的过程,需要注意的是分析过程中的cat数组真实存在,ans数组则是虚拟的。
中位数是排名在中间的数,所以我们要判断一个数是不是中位数,则应当计算它的名次。
相应地,我们只需获得名次在中间的数,就得到了中位数。

对cat数组排序,取最小距离l=0和最大距离r=cat[n-1]-cat[0],取两者均值
判断均值在所有距离中的名次:

  1. 均值名次大于n(n-1)/2则说明均值大于中位数,那么让最大距离r等于均值,这样中位数仍然在lr范围内且区间已经逼近中位数,r更靠近中位数
  2. 均值名次小于n(n-1)/2则说明均值小于中位数,那么让最小距离l等于均值,这样中位数仍然在lr范围内且区间已经逼近中位数,l更靠近中位数。

如此反复,最终我们会让lr准确地落在中位数上。

那么接下来新问题出现了,我们如何取得均值的名次呢?首先,距离数组ans是不存在的,这意味着我们要用一些巧妙的做法把均值在ans数组中的名次从cat数组中求出来。做法如下:
将cat排序,取两索引i<j,则cat[j]-cat[i]>0

//穷举所有i与j计算出所有小于等于avg的dis,dis数量就是avg的名次。 
int i = 1;
int rank = 0;
for(int j = 2;j<=n;j++){
	while(a[j]-a[i]>avg) i++;//a[j]-a[i]大于l与r的均值avg,要使a[j]-a[i]逼近avg,就要将i右移,让dis减小
		rank += (j-i);//对于每个j,cat[i]与cat[j]之间所有数到cat[j]的距离都小于cat[i]到cat[j]的距离,因此将他们一并计算名次即可。 
}

好了,如何获取中位数,如何获取一个数的在ans中的名次这两个问题我们都解决了,思路完毕。

心得

总的来说,这一部分的题目感觉思维量上来了,需要对题目的规律进行分析,这个题最为明显。
做了得有…两三个小时吧,因为一开始怎么都理解不了题目。

代码
#include<cstdio>
#include<algorithm>
#define LL long long
using namespace std;
int cat[10000010];
int n;

int getRank(int avg) {
	//ÇóÃû´Î
	int i = 1;
	int rank = 0;
	for(int j = 2; j<=n; j++) {
		while(cat[j]-cat[i]>avg)
			i++;
		rank += (j-i);
	}
	return rank;
}
int main() {

	while(~scanf("%d",&n)) {
		for(int i = 1; i <= n; i++) {
			scanf("%d",cat+i);
		}
		sort(cat+1,cat+n+1);


		int l = 0;
		int r = cat[n];
		int midpos = ((n-1)*n/2 + 1) / 2;
		while(l<r) {
			int avg = (l+r)>>1;

			if(getRank(avg)>=midpos) {
				r = avg;
			} else {
				l = avg+1;
			}
		}
		printf("%d\n",l);
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值