第四周 贪心算法与整数二分问题

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

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

输入样例
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

输出样例:
0
3
5

问题分析:
1.构造作业对应的节点类型,由题目叙述可知,作业由时间变量与得分变量两个变量进行描述。
2.关于如何选择才可使扣分最小。
利用贪心算法。扣分最小就是得分最大,而选择题目进行得分的冲突在于相同DDL下如何进行选择。
采用贪心的思想在于,得分大的先进行选择占位,这样可以解决同DDL的冲突。
其次,在为一个得分选择时间时,选择其DDL之前的天数均可。
在此种情形下,选的天数越靠后,越占优。因而从DDL开始往前找空余天数即可。
3.程序的实现:
在进行题目分数初始化时,将题目总分统计到sum中。
将分数数组按得分情况进行排序,然后不断遍历,为题数选天。
选择过程为:从该题目DDL开始往前找,找到第一个空闲天,则该天得分为该题分数。结束,再选下一题。
开辟一个数组存放可进行得分的题目分数,最后对得分数组进行统计,总分为得分。
最小扣分=总分-最大得分。

#include 
#include 
#include 
using namespace std;

int a[10000];

struct node
{
	int ddl;
	int score;
};

bool compare(node a,node b)
{
	return a.score>b.score;            //将扣分大的排在前面 
}

int func()
{
	int n,sum=0; 
	cin>>n;
	                //从a[1]到a[n]对分数进行存放 
	node s[n+1];
	
	for(int i=1;i<=1500;i++)
	{
		a[i]=0;
	}
	 
	for(int i=1;i<=n;i++)
	{
		cin>>s[i].ddl;
	}
	
	for(int i=1;i<=n;i++)
	{
		cin>>s[i].score;
		sum+=s[i].score; 
	}
	
	sort(s+1,s+n+1,compare);
	
	for(int j=1;j<=n;j++)
	{
	 for(int i=s[j].ddl;i>=1;i--)
	 {
	      if(a[i]==0)
		  {
		   a[i]=s[j].score;
		   break; 
	      }
	    
	 }
    }
	
	int sum1=0;
	for(int i=1;i<=1500;i++)
	{
		sum1+=a[i];
	}
//	cout<
	
	cout<<sum-sum1;	
} 

int main()
{
	int t;
	cin>>t;
	for(int i=0;i<t;i++)
	{	
		func();	
		cout<<endl;	
	}
	return 0;
	
	
}

时间复杂度的分析:
题的核心思想在于n个数的n次前寻,因而时间复杂度近似于O(n^2)。

关于hard版的优化方案:
实现思路为:从最大的DDL开始,为每一天匹配对应的点。
该天对应的点–>DDL小于等于该天的所有点中得分最大的那个。
因而实现思路为从最大天开始,不断向前操作,将对应天的元素入队,然后不断选队顶 元素。

4.2 整数二分问题的求解
给你四个数列A,B,C,D。从每个数列中各取出一个数,使4个数的和为0.
当一个数列中有多个相同的数字的时候,把它们当做不同的数对待

Input第一行:n(代表数列中数字的个数) (1≤n≤4000)
第二行:依次输入A,B,C,D(输入一个A再输入一个B)
Output输出不同组合的个数。

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

输出样例:5

本题中的符合条件的数对为:
(-45, -27, 42, 30), (26, 30, -10, -46), (-32, 22, 56, -46), (-32, 30, -75, 77),
(-32, -54, 56, 30).

理解:
对于二分的题目我们很容易想到一般的解决方法 对于这道题
如果使用暴力枚举的话复杂度将会到达N的四次方 虽然数据规模不大
但一定是不可以的 所以我们可以换个思路想问题
我们可以把前两个数组和后两个数组分别看做一个数组
然后前两个数组任意两个相加 得到一个 两个数组和 的数组 后两个一样
然后进行二分查找 如果为零总数加一

注意:
这道题里面需要注意的一个点是有可能出现相加后的数有可能有相同的且和后面相加为零
这种情况我们需要考虑 代码中用一个while来考虑这种情况
还有 我们其实只需要对后面那个数组进行排序就可以了

关于二分的实现思路说明:
suma[i]+sumb[mid] 与 0 相比共存在 3种情况。
当suma[i]+sumb[mid]>0时,向前找,即右区间缩小。
当suma[i]+sumb[mid]=0时,向前找,即右区间缩小。
当suma[i]+sumb[mid]<0时,向后找,即左区间缩小。
因而,所有三种情况进行两种处理。
当向后找时,此时mid点肯定不是所求点,因而 start=mid+1,新区间点跳出该点。
当向前找时,此时mid点可能是所求点, 因而 end=mid, 新区间包括该点。

关于循环的跳出:当区间中只剩下一个点时,即start=end时,循环跳出进行下一步判断。
找出的点为:满足条件suma[i]+sumb[mid]>=0时,mid的下标最小的情况。

#include 
#include 
using namespace std;

int x1[4000],x2[4000],x3[4000],x4[4000];
int suma[16000000];
int sumb[16000000];

int  ans=0,num;

void number(int n)
{
	int count=0;
	int start,mid,end;
	
	for(int i=0;i<n;i++)
	{
		cin>>x1[i]>>x2[i]>>x3[i]>>x4[i];
	}
	
	for(int i=0;i<n;i++)
	{
		for(int j=0;j<n;j++)
		{
			suma[count]=x1[i]+x2[j];
			sumb[count]=x3[i]+x4[j];
			count++;
		}
	}
	//此循环结束后count大小为 n^2
	
	//在sumb中进行二分查找,因而使其有序
    sort(sumb,sumb+count);                //区间为【0,n^2)	
	
	//进行二分查找统计的循环
	for(int i=0;i<count;i++)
	{
	   start=0;
	   end=count-1;
	   
	   while(start<end)              //二分结束的判断条件,最后一步是start与end取等 
	   {
	   	 mid=(start+end)/2;
		 if(suma[i]+sumb[mid]>=0)
		 end=mid;   //满足=0的值放于end中,但最后一步start与end取等,最终放在start中 
		 else
		 start=mid+1;
	   } 
	   while(suma[i]+sumb[start]==0 && start<count) 
	    //start存放的是第一个满足条件的数 
		//防止在第二个数组内有相同的数与suma[i]相加等于零不被统计
		//加入sumb中最大值也不满足,则最后start=count-1; 
		{
			ans++;
			start++;             
		} 
    }
	   cout<<ans<<endl; 
} 
 
int main()
{
	while(cin>>num)
	{
		number(num);
	}
}

4.3 数组元素互减的绝对值数列中位数
题目:
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 位置对应的数字,’/’ 为下取整。

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

分析:1.构造的新数组中存在[n*(n-1)]个数。
2.中位数满足的条件为在有序数组中,该数后面有[n*(n-1)/2]个数。

比如:1 2 3 的中位数为 2; 1 2 3 4 5 6 的中位数为 3

做法1:暴力枚举
枚举i, j将数列B计算出来,然后排序取出中位数;
时间复杂度 n方,数据范围较大,难以接受。

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

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

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

举例: 1 1 1 3 3 4
给定数为2,但中位数为1,因此给定的数为虚数,需判断其与中位数的关系。

结论:满足单调性,可以二分P!

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

那么如何计算名次呢?
其中 P是一个虚数,它是答案范围内的数,但可能并不处于数组之中。
我们统计P之后的数目,就是大于等于P的数目。这个数目我们记为count。
count > n*(n-1)/4 的时候,证明P小了,P向后取值,更新左区间。
count = n*(n-1)/4 的时候,证明P大了,P向前取值,更新右区间。
count < n*(n-1)/4 的时候,证明P大了,P向前取值,更新右区间。
因而分为两种可能的情况:更新左区间;更新右区间

l=mid+1 向后取值时,此时mid可能为所求值
r=mid-1 向前取值时,此时mid不可能为所求值

mid实际上是范围内的一个虚数,可能不在数组中。
因而当 l>r 时循环才会跳出,当l=r时,会进行一次虚数转化为数组中的数的过程。
当非虚数时,l存放的就是所求,但 l进行了 l++;
当是虚数时,l存放的mid实际上是一个比中位数大1的数。
因而最终输出(l-1)即是对最终结果的输出。

关于lowerbound(a,a+n,x) 通过二分查找在数组a中从下标为[0,n)的数中查找。
返回第一个大于等于x的地址。
通过 lowerbound(a,a+n,x)-a 可得到数组中小于 x的数的数目。

#include
#include
#include
using namespace std;
const int N = 100005;
int a[N], n, m;

bool judge(int x) 
{
    int count = 0;
    for (int i = 0; i < n; i++)
    {
        int num=n-(lower_bound(a, a + n, a[i] + x) - a);
        count+=num;
    }
	return count > m ? true : false; 
}

int main() 
{
	int mid; 
    while (~scanf("%d", &n)) 
	{
        m = n * (n - 1) / 4;
        for (int i = 0; i < n; i++)
            scanf("%d", &a[i]);
        sort(a, a + n);
        int l = 0, r = a[n - 1];
        while (l <= r) 
		{
            mid = (l + r) / 2;           
            if (judge(mid))
                l = mid+1;
            else
                r = mid-1;
        }
        printf("%d\n", l-1);
    }
    return 0;
}
 
```cpp




你可能感兴趣的:(程序设计实验)