【挑战程序设计竞赛(第二版)】2.2【一直向前,贪心法:硬币问题、区间问题、字典序最小等例题】

贪心法

思想:不断地找到当前最优的解。
原来学习的笔记来自OneNote

题目一 硬币问题

硬币问题题目描述
题外话:这是一个不尽相异的组合问题,A={13,52,101,503,1000,5002},
组合数=11!/(3!*2!*1!*3!*0!*2!)
解题思路:尽可能的先选大面值的硬币,直到不能分的时候,开始分第二大面值的硬币,…,直至分完。

/*
input:
3 2 1 3 0 2 620
output:
6
*/
#include<iostream> 
using namespace std;

// 硬币面值
const int C[6]={1,5,10,50,100,500};
// 硬币个数
int Cnt[6] ;
// 目标数值 
int aim;
// 总共需要的硬币数
int count = 0; 

int main(){
	cout<<"依次输入六个面值硬币的个数和目标数值:"<<endl;
	for(int i=0;i<6;i++){
		cin>>Cnt[i];
	}
	cin>>aim;
	for(int i=5;i>=0;i--) {
		int t = min(aim/C[i],Cnt[i]); //使用第i个硬币的个数
		aim -= t * C[i] ;
		count+=t;
		// 以下是我最初的解题,时间消耗不如上面的,代码也不够精简
//		for(int j=0;j<Cnt[i];j++){
//			if(aim>=C[i]) {
//				cout<<C[i]<<endl;
//				aim-=C[i];
//				count++;
//			}
//		}
	}
	cout<<"需要的硬币数:"<<count<<endl; 
	return 0;
}

题目二 区间调度问题

在这里插入图片描述在这里插入图片描述
要求是选择尽可能多的工作,基于贪心的想法,加上本题给出的例子,每次选取开始时间最早的工作
但是对于开始的早的,结束时间很晚的,这种策略就是错的。如下图:
在这里插入图片描述
因此,要仔细思考一种合理的算法:

  1. 在可选的工作中,每次选取结束时间最早的
  2. 在可选的工作中,每次选取用时最短的
  3. 在可选的工作中,每次选取与最少工作有重叠的工作
    算法2、3都有反例,算法1是正确的算法二三的反例
/*
input:
5
1 2 4 6 8
3 5 7 9 10
*/
#include <iostream>
#include<algorithm>

using namespace std;

const int MAX_N = 100000;

// 事件数,开始时间,结束时间 
int N,S[MAX_N],T[MAX_N];

// 用于对事件排序的pair数组
pair<int, int> itv[MAX_N];

void solve(){
	// 将事件的起始、终止时间存入pair数组中
	// pair:字典序排序 
	// 为了便于pair对 T 进行排序 ,将 T 存入first中 
	for(int i = 0; i < N; i++){
		itv[i].first = T[i];
		itv[i].second = S[i];
	}
	sort(itv, itv + N);	//sort 中存入数组的物理首尾地址即可 ,调用algorithm 
	
	int ans = 0, t = 0; 
	for(int i=0;i<N;i++){
		if(t<itv[i].second) {
			ans++;
			t = itv[i].first;
		}
	} 
	cout<<ans;
}
int main(){
	cin>>N;
	for(int i=0;i<N;i++){
		cin>>S[i];
	}
	for(int i=0;i<N;i++){
		cin>>T[i];
	}
	solve();
	return 0;
}

友情link:pair详解 sort函数用法详解
对于pair方便将两个数据组合起来排序的说法,我想到了结构体,这里是我原来关于sort()的文章。sort()相关题目
在这里插入图片描述

题目三 字典序最小问题[/(ㄒoㄒ)/~~又是近两小时消耗]

在这里插入图片描述
Question:神马是字典序呢?
Answer:比较两个字符串,先比较第一个字符,如果不同,首字母小的整个字符串就是小的;如果相同,用相同的规则继续比较。就像咱们小时候查字典一样的嘛,a开头的单词就在b开头的单词前头,apple就在anaconda的后头。

规则:每次取 B 的开头或结尾放到 T 的后面
目标:T 前面的字符串尽量小就ok
考虑:每次取 B 的开头和结尾的最小元素放到 T 的末尾 ,应该可以得到很好的解,
但是当遇到两边元素相同的时候,如何做取舍呢? 我觉得这时应该前后指针为往里移一个,
判断此时的元素那个小,就选取哪边的元素放到 T

比如说,ACDBCB,第一次(A,B)中取A,第二次(B,C)中取B,第三次(C,C)中如果取了pre标记的C,下一次只能从(C,D)中取到C,而如果取了post标记的C,下一次就是从(C,B)中得到的B。所以此处遇到相同元素显然要先判断一下里面元素的相对大小。

以下程序是我写的烂包,把简单问题搞乱了。
考虑到元素相等时的取舍问题,思路是好的,往里判断是对的,但是不需要里面元素比较的结果,此次筛选只是为了知道遇到相同元素时,如何选取对下一次能选到最优的解有帮助,所以只需要判断完成标记即可,不需要用递归将pre+1,post-1传入计算。

/*
input:
6 ACDBCB
output:
ABCBCD 
*/ 
#include<iostream> 
#include<algorithm>
using namespace std;

const int MAX_N = 1000;

int N;
char S[MAX_N],T[MAX_N];

char T_value(int pre,int post) {
	char temp = ' ';
	int pre = 0, post = N-1;
	if(pre <= post){
		if(S[pre] > S[post]){
			temp = S[post];
			post -= 1;
		}
		if(S[pre] < S[post]) {
			cout<<pre<<":"<<S[pre]<<"  "<<post<<" :"<<S[post]<<endl;
			temp = S[pre];
			pre += 1;
		}
		if(S[pre] == S[post]){
			temp = S[pre];	
			T_value(pre+1,post-1);
		}
	}
	return temp;
}

int main(){
	cin>>N;
	for(int i=0;i<N;i++){
		cin>>S[i];
	}
	for(int i=0;i<N;i++){
		T[i] = T_value();
	} 
	for(int i=0;i<N;i++){
		cout<<T[i];
	}
	return 0;
}

这里是书上的程序,确实又精简又好用:
用left标记是否输出的是左边的元素。
在if判断中没有相等的情况,所以就进行循环,使pre和post同时向里移一位,继续比较,也不改变pre和post的大小。【下面的a,b就是我写的pre和post】

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

const int MAX_N = 1000;

int N;
char S[MAX_N];

void solve() {
	int a = 0, b = N-1;
	while(a <= b) {
		bool left = false;	// 此处标记是否取的是左边的元素
		for(int i = 0; a + i <= b; i++) {
			if(S[a + i] < S[b - i]) {
				left = true;
				break;
			} else if(S[a + i] > S[b - i]) {
				left = false;
				break;
			}
		}
		if(left) 
			putchar(S[a++]);
		else 
			putchar(S[b--]);
	}
}

int main(){
	cin>>N;
	for(int i = 0; i < N; i++){
		cin>>S[i];
	}
	solve();
	return 0;
}

题目四 Saruman’s Army(POJ 3069)

在这里插入图片描述
循环次数很明显是n次,将最左边的点标为s。在这里插入图片描述

/*
Saruman's Army(POJ 3069)

input:
6 10
1 7 15 20 30 50
output:
3
*/
#include<iostream> 
#include<algorithm> 
using namespace std;
const int MAX_N = 100;
int n,r;
int a[MAX_N];

void solve(){
	sort(a,a+n);	// 将任意给定的点进行排序 
	int i = 0,ans = 0;
	while (i < n) {
		int s = a[i++];	//s记录unmarked最左边的点 	s = 1 
		while (i < n && a[i] <= s+r) i++;	// i = 1 一直向右边找距离大于r的点 i = 2
		int p = a[i - 1] ;	// p是新加上标记的点的位置 	p = 7
		while (i < n && a[i] <= p+r) i++;	// i = 2 继续向右 i = 3
		cout<<s<<"  "<<p<<endl;
		ans++; 
	}
	cout<<ans;
}
int main(){
	cin>>n>>r;
	for (int i = 0; i < n; i++) {
		cin>>a[i];
	}
	solve();
	return 0;
}

题目五 Fence Repair(POJ 3253)

在这里插入图片描述
第一个思路:
【5、8、8】或者【8、5、8】,每次将最大的切下来,那剩下的再划分,最大的算价钱就只需要算一次了,因为越晚划分成独立的小块,随着次数的增加要算n次钱。
第一?:分成【8、5】和【8】,价钱P:8+5+8
第二?:【8】【5】,价钱P:P+8+5
所以,思路应该是先把想要的最大的独立块切下来,小的再从剩余的木料中慢慢切分。
换个例子【1,2,3,4,5】验证一下。
第一?:【1,2,3,4】,【5】,价钱P:15
第二?:【1,2,3】,【4】,价钱P:15+10
第三?:【1,2】,【3】,价钱P:15+10+6
第四?:【1】,【2】,价钱P:15+10+6+3 = 34
再仔细思考一下,每一次付钱只能切一刀,一刀只能得到两个小块,所以,每次切的时候都考虑最终需要的独立的小块正确吗?答案是不正确。比如上一个例子,花15块钱切第一刀,将15分成10(需要切割的)和5(是独立的),意味着再下一次要付10块钱来继续划分成别的小块,这不是很亏吗?相当于多算了好几次小块的钱。
所以,改变思路:

  1. 求出sum[L1,L2,…,Ln]
  2. 找出其中能将左右基本分均匀的点,也就是左右两边的和要接近sum/2
    然后验证之后也是有误差的,求出来不是最优解,存在小块如何分成两个不同的组的问题,很小的块不会影响左右和的稳定,但是会影响下一步切割,所以参考了书上的思路。
    书上的思路,将分割的方法用二叉树表示:
    二叉树表示
    最终的总代价=Σ结点*层数=(3+4+5)*2+(1+2)*3 = 33
    并且最小的两个结点应该是兄弟结点,位于最底层。
    课本思路描述课本思路描述
    为什么3 4 成兄弟了呢?按照以上理论,不应该是(1+2)和3是兄弟吗?下面是我修改的图:
    按我理解修稿的图
    这是我的理解画的图,最终的总价格是一样的,计算结果没变。这个就是构建最小二叉树的思想。
/*
Fence Repair(POJ 3253)
input:
5
5 1 2 4 3 
output:
33 
*/
#include<iostream> 
#include<algorithm> 
using namespace std;
typedef long long ll;

const int MAX_N = 100;
int n;
int a[MAX_N];

// 直到计算到木板为1块时为止 
void solve(){
	ll ans = 0;
	
	while (n > 1) {
		int s = 0, p = 1;
		if (a[s] > a[p]) swap(s, p);
		// s标记的最小,p标记的次小 
		for(int i=2;i<n;i++){
			if(a[i]<a[s]){
				p = s;
				s = i;
			}else if (a[i]<a[p]){
				p = i;			
			}
		}
		
		cout<<s<<"  "<<p<<endl;
		int t = a[s] + a[p];
		ans += t;
		if(s == n-1) swap(s,p);
		a[s]=t;	// 把上一步求的和放到s位上 
		a[p] = a[n-1]; // 要把遗落在n-1位上的元素搬到p来 
		n--;	// 然后数组小一位 
	}
	cout<<ans;
}
int main(){
	cin>>n;
	for (int i = 0; i < n; i++) {
		cin>>a[i];
	}
	solve();
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值