WEEK4——二分和贪心

Problem——DDL的恐惧

问题的描述

ZJM 有 n 个作业,每个作业都有自己的 DDL,如果 ZJM 没有在 DDL 前做完这个作业,那么老师会扣掉这个作业的全部平时分。

所以 ZJM 想知道如何安排做作业的顺序,才能尽可能少扣一点分。

请你帮帮他吧!

输入

输入包含T个测试用例。输入的第一行是单个整数T,为测试用例的数量。

每个测试用例以一个正整数N开头(1<=N<=1000),表示作业的数量。

然后两行。第一行包含N个整数,表示DDL,下一行包含N个整数,表示扣的分。

输出

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

问题分析

首先吧,咱建立一个结构体,用于保存每个作业的分值和ddl。(因为不用说出具体的顺序,序号什么的就不管了)
然后输入所有的作业情况后,我们要干的就是把所有的作业按照分值的大小,由高往低排列。一样分值的再按照ddl小的进行排列。
排列完成后,按照这个顺序,挨个对作业进行处理,假设第m个任务的ddl为x,那么就优先把这个作业排在x那一天。如果那一天已经被其他作业占用,就往前找,找到一个小于ddl又是空闲的天数,确定下来。如果已经找不到小于ddl且空闲的天数,那么这个作业就舍弃掉,它的分值就加入到减少的分里面去。重复此操作,直到所有能安排的作业都已经安排好,即可。

代码

#include<iostream>
using namespace std;
#include<algorithm>
struct work
{
	int deadtime;
	int gread;
};
bool cmp(work& a,work& b)
{
	if(a.gread!=b.gread)
		return a.gread>b.gread;
	else
		return a.deadtime>b.deadtime;
}
int vis[1000];
int N;
void innitial()
{
	for(int i=1;i<=N;i++)
		vis[i]=0;
}
bool insert(int ddl)
{
	int i; 
	for(i=ddl;i>=1;i--)
	{
		if(vis[i]==0)
			break;
	}
	if(vis[i]==0&&i>=1)
	{
		vis[i]=1;
		//cout<<i<<"    "<<ddl<<endl;
		return true;
	}
	else
		return false;
}
int main()
{
	int T;
	int sum;
	cin>>T;
	
	work works[1001];
	while(T--)
	{
		
		cin>>N;
		sum=0;
		for(int i=0;i<N;i++)
		{
			cin>>works[i].deadtime;
		}
		for(int i=0;i<N;i++)
		{
			cin>>works[i].gread;
		}
		sort(works,works+N,cmp);
		innitial();
		for(int i=0;i<N;i++)
		{
			if(!insert(works[i].deadtime))
				sum=sum+works[i].gread;
		}
		cout<<sum<<endl;
		
	}
	
	return 0;
} 

遇到的问题

对于数组的循环处理时,数组的端点还是没有处理的太好,都是后来拿数据试才找到究竟该怎么写。

Problem——四个数列

问题的描述

ZJM 有四个数列 A,B,C,D,每个数列都有 n 个数字。ZJM 从每个数列中各取出一个数,他想知道有多少种方案使得 4 个数的和为 0。

当一个数列中有多个相同的数字的时候,把它们当做不同的数对待。

请你帮帮他吧!

输入

第一行:n(代表数列中数字的个数) (1≤n≤4000)

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

输出

输出不同组合的个数。

问题分析

首先先遍历a,b数组,把他们的值相加,存到另一个数组里面去。
然后再给那个数组排个序。
遍历c,d数组,使用二分法在上面存放a+b的结果的数组里寻找-(c+d)(用left,mid,right,三个点来循环寻找改点第一次出现的时候,以及最后一次出现的时候)累加差值即可。
接下来详细的说一说这个让我头疼的二分法:
先取left和right的值,即存放a+b结果的那个数组的最左端和最右端。mid就是他们两个的中值。用mid来和要寻找的数作比较,因为我们这个是一个升序的数组,于是比mid大就在右边,就在mid,right的范围内再来一次二分。反之就是在left,mid的范围内再来一次二分。
而当寻找的数就是mid的时候,操作就有所不同了。
寻找第一次出现的时候,当mid=shu,我们就继续往mid的右边找,因为右边可能还有和数相等的数,前面有数就代表着这个mid所代表的数不是第一个出现的shu。就需要我们继续往右边找。
而找到最后一次出现的数的时候,结果就刚好相反,也是一样的思路。当mid=shu,很有可能mid的右边还有和shu相等的数,不一定mid就是最后一个出现的数,所以还需要往左找。
一当出现相等的数,就记录答案,再次出现相等的数,就更新答案,简单来说就是这样。

代码

#include<iostream>
using namespace std;
#include<algorithm>
int step=0;
int *a,*b,*c,*d;
int *all; 
int n;
int find_back(int shu)
{
	int left=1;
	int right=n*n;
	int ans=-1;
	//cout<<"back:    "<<shu<<endl;	
	while(left<=right)
	{
		//cout<<"left="<<left<<"   right="<<right<<"    ans="<<ans<<endl;
		int mid=(left+right)>>1;//除以2的意思
		if(all[mid]==shu)
		{
			ans=mid;
			left=mid+1;
		}
		else if(all[mid]>shu) 
		{
			//cout<<all[mid]<<"    mid="<<mid<<endl;
			right=mid-1;
		}
		else left=mid+1;
		//cout<<"front                       left="<<left<<"   right="<<right<<"   mid="<<mid<<"    ans="<<ans<<endl;
	}
	return ans;
}
int find_front(int shu)
{
	int left=1;
	int right=n*n;
	int ans=-1;
	while(left<=right)
	{
		int mid=(left+right)>>1;//除以2的意思
		if(all[mid]>=shu)
		{
			if(all[mid]==shu) 
			ans=mid;
			right=mid-1;
		}
		else left=mid+1;
	}
	return ans;
}
bool cmp(int f,int b)
{
	return f<=b; 
}
int main()
{
	
	
	cin>>n;
	a=new int [n+1];
	b=new int [n+1];
	c=new int [n+1];
	d=new int [n+1];
	all=new int [n*n+2];
	for(int i=0;i<n;i++)
	{
		cin>>a[i]>>b[i]>>c[i]>>d[i];
	}
	
	for(int i=0;i<n;i++)
	{
		for(int j=0;j<n;j++)
		{
			all[i*n+j+1]=a[i]+b[j];
			//step=step+find(-(a[i]+b[j]));
			//cout<<i*n+j+1<<"    a="<<a[i]<<"   b="<<b[j]<<endl;
		}
	}
	sort(all+1,all+n*n+1,cmp);
	//for(int i=1;i<=n*n;i++)
	//cout<<all[i]<<"  ";
		for(int i=0;i<n;i++)
	{
		for(int j=0;j<n;j++)
		{
			int shu1,shu2;
			shu1=find_back(-c[i]-d[j]);
			shu2=find_front(-c[i]-d[j]);
			//cout<<shu1<<"     "<<shu2<<endl;
			if(shu1!=-1)
			step=step+shu1-shu2+1;
			//cout<<"   c="<<c[i]<<"   d="<<d[j]<<"   front="<<find_front(-c[i]-d[j])<<"    back="<<find_back(-c[i]-d[j])<<endl;
		}
	}
	cout<<step<<endl;
	return 0;
}

遇到的问题

最开始以为自己懂了,写了半天发现还是n的4次方的复杂度。然后又多看了半天才理解到二分的用法。害,还是不太熟

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

输入

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

输出

输出新数组 ans 的中位数

问题分析

这道题的数据十分庞大,所以必须要使用二分法来减少运算量。
先输入所有数据到数组里,然后对数组进行排序操作。
我就设定了,small,large来表示a[j]-a[i]的最小值和最大值。mid代表他们的中值,是用来估计新的数列的中位数的大小的。
然后就是每次的mid就采用,a[j]-a[i]<mid的方法来统计小于这个“中位数”的个数,如果这个数的名次在中位数上就采用,否则就按照名次的高低和真正中位数的名次做对比,如果比需要的名次低,即我们预测的这个mid的数值有些小,就再次按照mid,large的范围来再次二分。否则就按照small,mid的范围来二分。
然后我发现,如果同时出现了多个和真正的中位数大小一样的数,只找比mid(预测的中位数)小的数,不能完全说清楚它的名词,于是我就用sum和sum1来分别记录,小于mid的数和等于mid的数。于是我就得到了所有值为mid的数的名次范围。用它再来和真正的中位数的名词次比较,就能够很好的得出结果了。

代码

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
int n;
int a[100005]; 


int findcnt(int i, int xi_p)
{
	int l=i+1, r=n;
	int m;
	if(a[l] > xi_p)	return 0;
	while(l < r)
	{
		m = (l+r)>>1;
		if(a[m] <= xi_p)	l = m+1;
		else r=m;
	}
	return l - (i+1);
}


bool judge(int p, int index)  // x的名次小于index则返回1, 反之返回0 
{
	int sum=0;
	for(int i=0;i<n-1;i++)
	{
		sum += findcnt(i, a[i]+p);
	}
	return sum < index;
}

int find(int index)
{
	int l=0, r=a[n-1] - a[0];
	int m;
	while(l<r)
	{
		m = (l+r)>>1;
		if(judge(m, index))	l = m+1;
		else r = m;
	} 
	
	return l;
}
int main ()
{
	while(~scanf("%d", &n))
	{
		for(int i=0;i<n;i++)	scanf("%d", &a[i]);
		int indexOfMid = ((n*(n-1) >> 1) + 1) >> 1;
		sort(a, a+n); 
		printf("%d\n", find(indexOfMid));
	}
	return 0;
} 

遇到的问题

我这个代码还有一些问题,在和其他同学交流过后,我才意识到,在寻找有多少个a[j]-a[i]<mid的时候也可以使用部分的二分法来简便运算,不过我过了,我就没有继续搞了。
还有我的方法记录了小于它的数和等于它的数,在名次的计算上那个值就不直接是名次,还需要做一些转换才行。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值