程序设计思维与设计——第三周作业(贪心:选数、区间选点、区间覆盖)

A-选数问题

题目如下:

Given n positive numbers, ZJM can select exactly K of them that sums to S. Now ZJM wonders how many ways to get it!
Input:
The first line, an integer T<=100, indicates the number of test cases. For each case, there are two lines. The first line, three integers indicate n, K and S. The second line, n integers indicate the positive numbers.
Output:
For each case, an integer indicate the answer in a independent line.
Example
Input:
1
10 3 10
1 2 3 4 5 6 7 8 9 10
Output:
4
Note:Remember that k<=n<=16 and all numbers can be stored in 32-bit integer。

思路分析:

这里是求有多少种组合使得k个数字的和为s,那很容易就能想到dfs深度搜索这个思路,dfs会选择某一种可能情况向前探索,在探索的过程中,一旦发现这个选择不符合要求,就会回溯到上一个状态重新选择另一种可能的情况,继续向前探索,如此反复进行,直至求得最优解。深度优先搜索可以结合栈使用,当发现不满足要求时,就会将最新加入栈中的元素弹出也就是回溯到原状态,也可以使用递归。这个题我采用的是递归。

void dfs(int deep, int cur, int sum)//分别表示当前已经选过多少个数字、当前所在的索引、当前总和 
{
	if(deep == k)//如果已经选了k个那么不能再选了,递归边界
	{
		if(sum==s)//结束递归后再判断是否满足要求
		    res++;
		return;//结束此次递归,进行回溯
	}
	if(cur>=n)//超出范围了,剪枝
	    return; 
	if(deep>k)//剪枝,表示获取的个数已经超过k个不符合题意,没必要再进行搜索
	    return; 
	for(int i = cur; i < n; i++) //从当前位置继续下一轮的递归 
	{
		if(n-cur<k-deep)//剪枝 如果当前最多能选取的数字数量还比实际需要选中的数字数量少,那么再搜也不会有结果 
		    return;
		dfs(deep + 1, i + 1, sum + a[i]);//进行下一种可能并对其dfs
	}
}

完整代码:

#include<cstdio>
#include<cstdlib>
using namespace std;

int n,k,s,res=0;
int a[16];
void dfs(int deep, int cur, int sum)//分别表示当前已经选过多少个数字、当前索引、当前总和 
{
	if(deep == k)//如果已经选了k个那么不能再选了 
	{
		if(sum==s)
		    res++;
		return;//结束此次递归 
	}
	if(cur>=n)//超出范围了  
	    return; 
	if(deep>k)
	    return; 
	for(int i = cur; i < n; i++) //从当前位置继续下一轮的递归 
	{
		if(n-cur<k-deep)//剪枝 如果当前最多能选取的数字数量还比实际需要选中的数字数量少,那么再搜也不会有结果 
		    return;
		dfs(deep + 1, i + 1, sum + a[i]);
	}
}
int main()
{
	int c;//要进行多少次的选数
	scanf("%d",&c);
	while(c--)
	{ 
	    scanf("%d%d%d",&n,&k,&s);//输入有多少个数字、要选多少个数字、要达到的总和
	    for(int j=0;j<n;j++)
	        scanf("%d",&a[j]) ;
	    dfs(0,0,0);
	    printf("%d\n",res);
	    res=0;
	}
	return 0;
}

题后反思:

dfs有着“不撞南墙不回头”的特点,所以我们一定要注意剪枝,以免浪费时间。如果不剪枝,稍微复杂的dfs的题就可能TLE。

B-区间选点(编译器选GNU G++)

(说句题外话,这道题我选的GNU G++,但是就是CE,我后来选了Microsoft Visual C++2017就AC了)

题目如下:

数轴上有 n 个闭区间 [a_i, b_i]。取尽量少的点,使得每个区间内都至少有一个点(不同区间内含的点可以是同一个)
Input:
第一行1个整数N(N<=100)
第2~N+1行,每行两个整数a,b(a,b<=100)
Output:
一个整数,代表选点的数目
Examples
Input:
2
1 5
4 6
Output:
1
Input:
3
1 3
2 5
4 6
Output:
2

思路分析:

区间选点问题,首先就会想到贪心算法,我们要保证每个区间都至少有一个点并且又要求点要最少,所以如果把区间按照右端点升序排序,如果右端点相同,就按照做左端点降序排列,然后取区间的最右边那一点,只需要每次取点的时候,判断是否取的这个点是否在下一个区间中,如果在,那么下一个区间就不需要再取点,如果不在,再取点。
按照右端点升序排序这个好理解,那为什么当右端点相等时左端点要降序排序呢?不妨举个例子,这里有四个区间[1,3],[4,9],[3,9],[2,9],这样一排列,当右端点相同时,后面的区间包含了前面的区间,也就是说,如果一个点在[4,9]内,那么这个点也在后面两个区间里面。

代码实现:

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

struct segment//表示区间
{
	int a,b;//分别表示区间的起点和终点 
	bool operator<(segment& s)
	{
		return b<s.b||(b==s.b&&a>s.a);
	} 
	segment& operator=(segment& s)
	{
		a=s.a;
		b=s.b;
		return *this;
	}
}seg[100];

int main()
{
	int n,point=0;
	while(~scanf("%d",&n))
	{
	    point=0;
		for(int i=0;i<n;i++)
 	    {
		    scanf("%d%d",&seg[i].a,&seg[i].b);//输入 
	    }
	    sort(seg,seg+n);
//	for(int i=0;i<n;i++)
//	    printf("a%d b%d   ",seg[i].a,seg[i].b);
//	printf("\n");
	    int getpoint=seg[0].b;
	    point++;//第一个区间的最后一个点是一定要取得 
	    for(int i=1;i<n;i++)//从第二个区间开始 
	    {
		    if(getpoint<seg[i].a)//前一个取的点不在接下来的区域里面 
		    {
		        getpoint=seg[i].b;//那就在这个区间里面取右端点
			    point++; 
		    }
	    }
	    printf("%d\n",point);
	}
	return 0;
} 

C - 区间覆盖(不支持C++11)

(这道题让我WA到怀疑人生!因为最开始应该是没有把题目意思吃透)

题目如下:

数轴上有 n (1<=n<=25000)个闭区间 [ai, bi],选择尽量少的区间覆盖一条指定线段 [1, t]( 1<=t<=1,000,000)。覆盖整点,即(1,2)+(3,4)可以覆盖(1,4)。不可能办到输出-1
输入:
第一行:N和T
第二行至N+1行: 每一行一个闭区间。

输出:
选择的区间的数目,不可能办到输出-1。
样例输入:
3 10
1 7
3 6
6 10
样例输出:
2

思路分析:

首先要说一句有关题意方面的问题,这个题中选出来的区间是可以超出指定区间的,比如[0,11]能覆盖[1,10]:[1,6]和[5,10]也完全可以覆盖区间。
这道题首先要进行预处理,也就是把那些完全不在指定区间的区间去除,比如指定区间[1,10]那么[20,30]是应该被除去的。我定义了一个vector类型的数组v来存储可以参与覆盖的区间:

  if((seg.a<=t&&seg.a>=1)||(seg.b>=1&&seg.b<=t))//进行预处理,筛选出[1,t]范围内的线段 
  {
	//v.insert(v.begin()+size,1,seg);//从0开始一次插入 
    v[size].a=seg.a;
	v[size].b=seg.b;
    size++;//统计v中存储了多少个元素
}

之后只需要对数组v进行处理就可以。将v中的元素按照左端点进行升序排序。之后,我们在每个可能被选中(该区间的左端点处于上一个被选中区间之间的区间或者刚好比上一个区间右端点大1的区间即有可能是下一个被选中的区间)的点中选取最适合的区间也就是右端点最大的区间。并且将这个右端点作为新的起始点,寻找下一个合适的区间。

代码实现:

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

struct segment
{
	int a,b;
	bool operator<(segment& s)
	{
	    return a<s.a;
	}
};
vector<segment> v(25000);//存储[1,t]以内的区间 
int main()
{
	segment seg;
	int n,t;
	int count=0;//统计 
	scanf("%d%d",&n,&t);
    int size=0;//vector中的元素数量 
	for(int i=0;i<n;i++)//输入线段 
	{
	///	fflush(stdin);/*清除输入缓冲区*/
		scanf("%d%d",&seg.a,&seg.b);
		if((seg.a<=t&&seg.a>=1)||(seg.b>=1&&seg.b<=t))//进行预处理,筛选出[1,t]范围内的线段 ?????????
		{
		    //v.insert(v.begin()+size,1,seg);//从0开始一次插入 
		    v[size].a=seg.a;
		    v[size].b=seg.b;
		    size++;//v中有这么多个元素 
		}
	}//将可以考虑的区间存在v中
	
	//现在进行排序 
	sort(v.begin(),v.begin()+size);
	
	//下面进行覆盖
	if(v[0].a>1)
	{
		printf("-1");
		return 0;//不用再进行下去
	}
	
	//找到起点是哪一个
	int j=0;
	int begin=v[0].b;//第一个区间的终点是这个 ,找做第一个点的最优 
	for(int i=1;i<size;i++)
	{
		if(v[i].a<=1&&v[i].b>begin)//有资格参与选择的区间
		{
			begin=v[i].b;
			j=i;
		}
		if(v[i].a>1)//由于是按照a升序排列的,那么这个区间不能被选择,那么之后的区间也不可能被选择
		    break;
	} 
	count++;
  // printf("起点(%d,%d)\n",v[j].a,v[j].b);
	while(begin<t)//只要每次选的右端点小于目的地就要继续选区间
	{
		int max=begin;//max表示上一个待考虑的区间的右边界,用来寻找这一轮中可选的右边界最大的区间 
	//	printf("begin:%d,max:%d\n",begin,max);
	    //j记录可以进去的区间索引 
		for(int i=j+1;i<size;i++)//当前起点从这个循环中寻找适合的下一个区间 
		{
		//	printf("(%d,%d)\n",v[i].a,v[i].b);
			if(v[i].a<=begin+1)//一定注意这里要+1
			{
				if(v[i].b>begin&&v[i].b>max)//可以考虑进入这个区间
				{
					max=v[i].b;//当前最大值
					j=i;//当前可能被选择的区间的索引
				}	 
			}
			else//a[i].b>begin+1  这就相当于后面没有了 所以break 
			    break;
		}
		if(max==begin)//如果max没有发生变动仍然保留进入循环前的值那说明没有能到达终点的可选的区间
		{
			printf("-1");
			return 0;
		}
	 //  printf("选中(%d,%d)\n",v[j].a,v[j].b);
		count++;
		begin=v[j].b;//新起点 
	}
	printf("%d",count); 
	return 0; 
} 

题目总结:

既然对区间按照a排好序,那就要好好利用起来,依照这个来剪枝,可以有效节省时间,之前没有充分剪枝(break/return)就TLE了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值