程序设计思维与设计——第四周作业(贪心:DDL多拿分;二分查找:选数使和为0、TT的神秘礼物)

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

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

思路分析:

这个题采用贪心策略,因为我们要保证多拿分,所以我们在可行的时间里总是寻找分数多的ddl完成,所以首先对输入的ddl按照分数的降序进行排序,然后依次将排列后的ddl的截止时间从后往前遍历,只要又遇到空闲的时间就来完成这个作业,注意:ddl截止时间当天完成也算是在期限时间前完成,并且初始时,也就是还没有安排ddl完成情况的时候,每一天都是空闲的,定义一个数组spare,初始为0,只要被安排完成作业,这一天就设定为1。
定义一个结构体ddl,数据元素为day,score分别表示ddl截止时间和该ddl的分数。进行降序排序后,执行以下操作:

for(int j=0;j<n;j++)
{
	int d;
	for(d=theddl[j].day;d>=1;d--)//在ddl当天以及之前完成都可以 
	{
		if(spare[d])//如果这一天是空闲
		{
		   spare[d]=0;//这一天被安排
		   break;//表示不用扣分,直接跳出循环,进行下一个ddl的安排
		}
	}
	if(spare[d])//每个ddl安排完毕后,如果未占据 
		drop+=theddl[j].score;//说明这一天没有占据任何一天,也就丢掉这个分数
} 

代码实现:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

struct ddl
{
	int day,score;
//	bool free;
	bool operator<(ddl &d)
	{
	    return score>d.score;
	}
}theddl[1001];

int spare[5000];

int main()
{
	int count;
	scanf("%d",&count);
	for(int i=0;i<count;i++)
	{
		memset(spare,1,5000);//每一轮都要进行初始化 
		int n;
		int drop=0;
		scanf("%d",&n);
		for(int j=0;j<n;j++)
		    scanf("%d",&theddl[j].day);//输入ddl 
		for(int j=0;j<n;j++)
		    scanf("%d",&theddl[j].score);//输入对应分数 
		    
		sort(theddl,theddl+n);//降序排列
		
		for(int j=0;j<n;j++)
		{
			int d;
			for(d=theddl[j].day;d>=1;d--)//在ddl当天以及之前完成都可以 
			{
				if(spare[d])
				{
				    spare[d]=0;//表示占满 
					break;//表示不用扣分 
				}
			}
			if(spare[d])//如果未占据 
			    drop+=theddl[j].score;//这个分就不要了 
		} 
		printf("%d\n",drop);
	}
	
	return 0;
}

A题总结:

这个题我最开始找错了贪心指标,我将结构体ddl首先按照截止时间的降序排列,如果截止时间相同,就按照分数的降序排列。自然而然认为:这样可以保证在有限的时间内完成尽量多的ddl,并且时间冲撞的一天还可以选择分数大的完成。
但是!!!忽略了一点:这道题完全不用按照时间的先后顺序做题,完成尽量多的题不代表就可以获得尽量多的分数,如果一道题分值大的但是ddl靠后的,按照以上的错误思想处理后,很有可能就没有时间可以完成这道分值大的题。这显然是不对的!!!

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

思路分析:

这道题肯定不能暴力叭,如果复杂度达到O(n^4),这样的代码可真是有点糟糕。
我们可以想想,什么样的数相加会等于0呢,那我肯定想到的是互为相反数的两个数字,巧了!这里有四个数列,那我就可以把前两个数列和后两个数列的所有的和情况计算并且存储,再在剩下的两个数列中找到与计算出来的那个数互为相反数的和。计算前两个数列的和的复杂度是O(n^2),之后就着算出来的这么多的数字去另外两个数列的和中寻找相反数,就可以采用二分查找的思路:

int find(int x,int k,int *&cd)//参数分别表示带查找的数字、cd数列和的数组的元素个数以及cd数组的和数组
{
	int l=0,r=k-1,ans=0;//l表示左端点索引,r表示有端点索引
	while(l<=r)
	{
		int mid=(l+r)>>1;//除以2
		if(cd[mid]>=x)
		{
			ans=mid;//从这个地方开始一直往左 
			r=mid-1;//去左部分 
		}
		else
		    l=mid+1;//去右半部分 
	}
	return ans;//返回索引 满足条件的最小索引 之后在主函数中会从这个位置往右遍历 
}

注意:这个ans的值表示的是cd的和的数组中等于x的最左边的元素索引。所以,之后,必须从这个索引开始往右遍历寻找是否还有并列的数字。

代码实现:

#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;

int n;//每组数列有多少个数字
int count=0;
int a[4000],b[4000],c[4000],d[4000];

int find(int x,int k,int *&cd)
{
	int l=0,r=k-1,ans=0;
	while(l<=r)
	{
		int mid=(l+r)>>1;//除以2
		if(cd[mid]>=x)
		{
			ans=mid;//从这个地方开始一直往左 
			r=mid-1;//左部分 
		}
		else
		    l=mid+1;//右半部分 
	}
	return ans;//返回索引 满足条件的最小索引 之后在主函数中会从这个位置往右遍历 
}

int main()
{
	scanf("%d",&n);
	int cap=pow(n,2);
    int *sumab=new int[cap];
    int *sumcd=new int[cap];
	
	int count=0;//组合个数 
	for(int i=0;i<n;i++)
	    scanf("%d%d%d%d",&a[i],&b[i],&c[i],&d[i]);
	int k=0;//sumab的索引 
	for(int i=0;i<n;i++)
	    for(int j=0;j<n;j++)//计算ab的所有和的情况
	    {
	        sumab[k]=a[i]+b[j];
	        sumcd[k++]=c[i]+d[j];
	    }
	
	//之后为sumab找相反数,把C,D的所有组合的和算出来并且排序,与sumab匹配的时候 采用二分查
	sort(sumcd,sumcd+k);//排序
//	for(int i=0;i<k;i++)
//	    printf("sumcd:%d ",sumcd[i]);
//	printf("\n");
	
	for(int i=0;i<k;i++)//二分查找相反数 
	{
		int x=0-sumab[i];
	//	printf("x:%d,",x);
		int index=find(x,k,sumcd);
	//	printf("index:%d\n",index);
		while(index<k&&sumcd[index++]==0-sumab[i])//从这个位置向右遍历查找与之相等的数字 
		    count++;
	}
	printf("%d",count);
	delete []sumab;
	delete []sumcd;
	return 0; 
} 

B题总结:

这个题比较简单,就是比较直白的整数二分查找问题,只不过最后要记得从获得的返回值索引开始往右遍历寻找是否有并列的数字,切记切记!!!

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 非常想得到那只可爱的猫咪,你能帮帮他吗?

思路分析:

这个题好难好难,就算知道用二分查找也完全不知道咋用,后来请教了大佬才知道(ಥ_ಥ)
言归正传,得讨论讨论怎么才能ac!
将这里的思路分析和下面的代码以及注释内容一起看,可能能够更加清晰明了一些。
将输入的cat数组进行升序排列,那要求的中位数一定一定在0~cat[n-1]-cat[0]这个范围内(包括边界)。 所以要在这个区间里进行二分查找我们要的数字(第一次遇到这种只知道范围不知道完整数组内容进行二分查找(•́へ•́╬))。
原题干中虽然对cat[i]-cat[j]要求i<j,但是鉴于是求绝对值,而且现在已经对cat进行升序排序了,所以大可以cat[j]-cat[i]并且i<j;设中位数为mid那么必然存在cat[i]+mid=cat[j]成立。定义一个rightcount变量表示对于我们设定的当前的中位数,cat[i]+mid=cat[j](其中i=0,1,2…n-1)成立的所有cat[j],在cat数组中等于或者大于它的个数。另外,我们要知道ans数组的元素个数为ans_size=n*(n-1)/2,中位数应该在的应该是数组中第midpos=(n*(n-1)/2+1)/2个,所以ans数组中中位数右边也就是等于或者大于中位数的个数就是ans_size-midpos;所以,如果对于当前设定的mid,所有cat[0:n-1]+mid统计完毕后的rightcount大于ans_size-midpos那么说明我选的mid偏小,所以中位数的左边界就成为mid+1,否则说明我们的mid偏大或者刚好合适,那么右边界就应该是mid(这种情况之后,继续往左寻找直到l<r,);
注意:(1)二分查找函数最后不能返回r,因为正如上面所说,r=mid的时候,rightcount可能小于ans_size-midpos也可能等于!!!所以要返回l-1!!!
(2)有个难点就是如何计算出cat中比cat[0:n-1]+mid大或者相等的树的个数呢,为此特意百度了一个查找数字返回地址的函数:

for(int i=0;i<n;i++)
{
   rightcount+=n-(lower_bound(cat,cat+n,cat[i]+mid)-cat);//表示此轮cat[i]在cat数组中于或者等于cat[i]+mid的数字有多少个 
//	        printf("cat+mid:%d,%d\n",cat[i]+mid,rightcount);
		   //获取这个第一个大于或者等于cat[i]+mid的位置 
	    }

lower_bound()是查找升序排列中第一个等于或者大于查找数的地址,用这个返回值减去cat首地址就可以找到个数了。再强调一遍:rightcount是每一轮cat中与cat][i]+mid相等或者比之大的值个数,i是从0开始到n-1,每一轮的总和啊!!毕竟ans中的内容是就是排序后的catcat[j]-cat[i](j>i)得来的啊。

代码实现:

#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;

//int cat[100000];//
int n;
//索引小的减去索引大的值,如果我升序排列,可以用索引大的减去索引小的,两者是等价的,后者还不用考虑绝对值 
int find(int n,int* &cat)//之前两道题都是对索引进行二分,这道题是对值进行二分 
{
	int l=0,r=cat[n-1]-cat[0],rightcount=0 ;//分别表示中位数的左右边界和第一个大于或等于某个数的右边数据的个数 
	int mid=0;//中位数 
	int midpos=(n*(n-1)/2+1)/2;//表示ans中所求中位数在第几个(从第一开始计数) 
	int ans_size=n*(n-1)/2;//表示ans数组中的数据个数 
//	printf("%d,%d\n",midpos,ans_size);
	
	while(l<r)//
	{
		mid=(l+r)/2;
//		printf("%d\n",mid);
		rightcount=0;
		for(int i=0;i<n;i++)
		{
		    rightcount+=n-(lower_bound(cat,cat+n,cat[i]+mid)-cat);//表示此轮cat[i]在cat数组中于或者等于cat[i]+mid的数字有多少个 
//	        printf("cat+mid:%d,%d\n",cat[i]+mid,rightcount);
		   //获取这个第一个大于或者等于cat[i]+mid的位置 
	    }
	    if(rightcount>(ans_size-midpos))//rightcount大于存储绝对值的数组中大于或者等于中位数的数字个数 
	        l=mid+1;
	    else//当前选的mid刚好合适或者偏大
	        r=mid; 
	}
	
	return l-1;//return r就出错了...... 
}

int main()
{
	int n;//数组容量 
	while(~scanf("%d",&n))//这里必须要有~号 
	{
		int *cat=new int[n];
		for(int i=0;i<n;i++) 
	        scanf("%d",&cat[i]);
	    
	    sort(cat,cat+n);//升序
	    int res=find(n,cat);
	    printf("%d\n",res);
	    delete[]cat;
	}
	return 0;
}

C题总结:

比较难,一开始完全没思路,这个知道区间来查找中位数的思路应该要好好收藏!!!
另外,我还犯了一个很蠢很蠢的问题,主函数中:

while(~scanf("%d",&n))//这里必须要有~号 

我一开始没有那个“~”,居然导致我output limited exceeded(我佛!!!)大家不要学我!
这种有两种方式:一种就是上面那种写法:另一种就是:

while(scanf("%d",&n)!=EOF)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值