最近看了《挑战程序设计竞赛》初级篇,这里总结一下部分poj上的练习题,主要涉及方面为:
穷竭搜索 and 贪心算法
具体题目:
一、穷竭搜索
穷竭搜索:顾名思义,就是暴力求解问题的一种,对于某些问题,解法不太明朗时,借助计算机的计算优势,把所有可能情况走一遍,在穷竭过程中得到问题的解的一种解法。
通常的实现方式:DFS or BFS
DFS(深度优先搜索):以深度为优先搜索,初始状态->下一个状态->…->状态无法在深入转移->回退前一步…
常常结合递归思想,代码实现上较简单。
具体习题POJ:
1.Red and Black
题目大意:输入一个N行M列的矩阵,其中.代表可以走的黑砖,#代表不可以走的红砖,@代表起点开始的黑砖,程序需要输入该矩阵从@出发所能走到的黑砖数。
思路:这是一道非常普通的DFS走迷宫题,从起点开始沿4个方向进行搜索(记得判断边界),为了避免回头,用一个数组记录走过的位置。
代码实现:
细节点:fill初始化二维数组;输入字符串的接收
#include<cstdio>
#include<algorithm>
using namespace std;
#define MAX_W 21
#define MAX_H 21
char road[MAX_W][MAX_H];
bool use[MAX_W][MAX_H];
int W = 1,H = 1; //行,列
int startW,startH;
int solve(int x,int y){
int res = 0;
if(x < 0 || x >= W || y < 0 || y >= H || use[x][y] == true) return 0;
if(road[x][y] == '#') return 0;
if(road[x][y] == '.' || road[x][y] == '@'){
res++;
use[x][y] = true;
}
res += solve(x+1,y);
res += solve(x-1,y);
res += solve(x,y+1);
res += solve(x,y-1);
return res;
}
int main(){
while(true){
scanf("%d%d",&H,&W);
if(H * W == 0) break;
fill(use[0],use[0] + MAX_W * MAX_H,false);//二维数组的初始化
for(int i = 0; i < W;++i){
scanf("%s",road[i]); //每行输入一个字符串
for(int j = 0;j < H;++j)
{
if(road[i][j] == '@'){
startW = i;
startH = j;
break;
}
}
}
printf("%d\n",solve(startW,startH));
}
}
2.curling
题目大意:特殊的冰壶比赛,需要从S(2)开始,达到G(3)点。规则大致为:
1.从2出发,选择一个方向前进
2.前进时遇到0可进,遇到1(障碍)会在障碍前,同时把障碍撞碎(把1变为0)
3.前进时方向不能中途改变,直到撞到边界或者障碍物(冰壶的滑行)
4.比赛最多允许走10次,求所有能从S到达G的走法中所用最少的步数。
思路:变种的迷宫题,思路上还是DFS,走4个方向,不过走的时候如果碰到了障碍,把障碍撞碎(把1变为0)后进行回溯的时候需要恢复障碍 ~
还有就是如果物体静止后右边有障碍物,不能直接把障碍物破坏(可以理解为至少要移动起来有动量才能撞碎),例如1 0 S 1 —> 0 S 0 1 --> 0 0 S 0 两步才能破坏右边障碍。
代码实现:
#include<cstdio>
using namespace std;
#define MAX_W 21
#define MAX_H 21
#define INF 100000
int H,W;
int road[MAX_W][MAX_H];
int startW,startH;
int shortPath;
int d[4][2] = {{1,0},{-1,0},{0,1},{0,-1}}; //四个方向
bool isBoard(int x,int y){ //出界判断
if(x < 0 || y < 0 || x >= W || y >= H) return true;
else return false;
}
void solve(int x,int y,int step){
if(step > 10) {//超过10步忽略
return;
}
for(int i = 0;i < 4;++i){ //四方向
int nx = x + d[i][0],ny = y + d[i][1];//下一步
while(!isBoard(nx,ny) && road[nx][ny] != 1){ //可以移动
if(road[nx][ny] == 3){
shortPath = (shortPath > step ? step : shortPath);
return;
}
//那只能是 road[nx][ny] = 0,2,尝试再走一步
nx += d[i][0];
ny += d[i][1];
if(isBoard(nx,ny)) break;//尝试其他方向
if(!isBoard(nx,ny) && road[nx][ny] == 1){ //如果下一步有砖,不能移动
road[nx][ny] = 0;//撞碎
solve(nx-d[i][0],ny-d[i][1],step+1);//回退前一步并走完这一步
road[nx][ny] = 1;//恢复撞碎现场
}
}
}
}
int main(){
while(true){
scanf("%d%d",&H,&W); //先列后行
shortPath = INF;
if(H * W == 0) break;
for(int i = 0; i < W;++i){
for(int j = 0;j < H;++j)
{
scanf("%d",&road[i][j]);
if(road[i][j] == 2){
startW = i;
startH = j;
}
}
}
solve(startW,startH,1);
if(shortPath == INF) //没能达到终点
shortPath = -1;
printf("%d\n",shortPath);
}
return 0;
}
BFS(宽度优先搜索):每次搜索据初始状态最近的状态,通常为了避免回溯,访问过的状态会进行标记,常常使用队列来实现,对比DFS,在递归深度不高时,BFS可能更占内存。
具体习题POJ:
1.MeteorShower
题目大意:
有M颗流星要撞击地球,流星i将在t时刻撞击点(Xi, Yi)。每颗流星都会破坏它撞击的点以及四个直线相邻的格点。现在你在时间0离开原点(0,0),以每秒一单位的速度平行于坐标轴,移动到任何尚未被流星摧毁的相邻直线点(被破的点不能走)。确定你到达安全地点的最短时间,如果逃不掉返回-1.
Sample Input
4 //有M颗流星
0 0 2 //Xi Yi T
2 1 2
1 1 2
0 3 5
Sample Output
5
思路:对于最短路求解,我们常常采用BFS,由于BFS按宽度优先,BFS第一个逃出来的肯定是步数最少的 (深度最小),BFS一般为了避免回溯,会记录走过的位置,这也减少了入队次数。
需要注意的是:如果一个点可能被周围多个流星破坏,应记录该点最早被破坏的时间
代码实现:
#include<cstdio>
#include<queue>
#include<algorithm>
using namespace std;
#define MAX_X 310
#define MAX_Y 310
#define INF 10000
struct P { //表示人现在的位置状态
int x;
int y;
int t;
};
int game[MAX_X][MAX_Y];
bool history[MAX_X][MAX_Y]; //不判断是否走过的话 会MLE! 内存超过
//在BFS先走到的肯定深度比后到的要小,用的步数少,这里不单单为了避免往回走(因为往回走game[nx][ny] > now.t + 1可以避免部分),还减少入队次数
int N;
int d[4][2] = {{1,0},{-1,0},{0,1},{0,-1}};
bool isBoard(int x,int y){
if(x < 0 || x >= MAX_X || y < 0 || y >= MAX_Y)
return true;
else return false;
}
int solve(){ //原点开始
queue<P> q;
P now;
now.x = now.y = now.t = 0;
history[0][0] = true;
q.push(now);
while(!q.empty()){
P now = q.front();q.pop();
if(game[now.x][now.y] == INF){ //逃出生天,一种结果,bfs第一个逃出来的肯定是最短的
return now.t;
}
for(int i = 0;i < 4; ++i){
int nx = now.x + d[i][0];
int ny = now.y + d[i][1];
if(!isBoard(nx,ny) && game[nx][ny] > now.t + 1 && !history[nx][ny]){ //这个方向可以走
history[nx][ny] = true;
P next;
next.x = nx;
next.y = ny;
next.t = now.t + 1;
q.push(next);
}
}
}
return -1;
}
int main(){
fill(game[0],game[0]+ MAX_X * MAX_Y,INF);
fill(history[0],history[0]+ MAX_X * MAX_Y,false);
scanf("%d",&N);
for(int i = 0;i < N;++i){
int x,y,t;
scanf("%d%d%d",&x,&y,&t);
if(game[x][y] > t) //存要是最小时间
game[x][y] = t;
for(int j = 0;j < 4;++j){
int nx = x + d[j][0];
int ny = y + d[j][1];
if(!isBoard(nx,ny) ){
if(game[nx][ny] > t) // 保存要是最小时间
game[nx][ny] = t;
}
}
}
printf("%d\n",solve());
return 0;
}
其他穷竭搜索题
1.Smallest Difference
题目大意:给定一组个位数(最多给10个),求由这一组数构成的两个数,的差值的最小值
例如:0 1 2 4 6 7 构成 204、176 差值最小 为28
思路:可以使用贪心来写,让大的尽量小,小的尽量大,而且我们很容易知道为了是这两个数最接近,我们会取位数最接近的来求( n / 2 拿一半位数)
对于这道题,贪心时间上简单但是写起来比较复杂,实际上直接穷举这组数构成的所有拿一半位数的组合数,取所有情况里差值最小的即可,时间要求上也不极限。
可以自己手写全排列(用DFS,记得回溯时复原),也可以直接用STL中的next_permutation获取下一个字典序较大的排列来取,对于每个排列,取前n/2位为第一个数,剩下的为第二个数。
代码实现:
#include<cstdio>
#include<algorithm>
#include <iostream>
#include <string>
using namespace std;
int N;
int num[10];
bool use[10];
int res = 10000000;
/*
1
0 1 2 4 6 7
*/
int numC(int a,int b){
int re = 0,x = 1;
for(int j = b; j >= a;j--)
{
re += x * num[j];
x *= 10;
}
return re;
}
void solve2(int i,int n){ //第i个定死,到第n-1个 自定义全排列
if(n == 2){ // 一方面剪枝,一方面避免 0 1,没有去计算
res = (res > abs(num[0] - num[1]) ? abs(num[0] - num[1]) : res);
return;
}
if(i >= n) //得到一种排列方式
{
if(num[0] == 0 || num[n / 2] == 0)
return;
int x = numC(0,n / 2 -1);
int y = numC(n / 2, n - 1);
res = (res > abs(x - y) ? abs(x - y) : res);
}
else{
for(int j = i; j < n;++j){ //注意,这个全排列跟next_permutation不一样,没有按字典走
int temp = num[j];
num[j] = num[i];
num[i] = temp;
solve2(i + 1,n);
int temp2 = num[j];//复原
num[j] = num[i];
num[i] = temp2;
}
}
}
void solve(int n){ //使用STL里的next_permutation(num, num+n)
int k = n / 2;
res = 10000000;
if(n == 2){
res = res > abs(num[0] - num[1]) ? abs(num[0] - num[1]) : res;
printf("%d\n",res);
return;
}
do {
if(num[0] == 0 || num[k] == 0)
continue;
int x = numC(0, k-1);
int y = numC(k, n-1);
res = (res > abs(x - y) ? abs(x - y) : res);
} while(next_permutation(num, num+n));
printf("%d\n",res);
}
int main(){
scanf("%d",&N);
getchar();//吃掉回车
string s;
while(N--){
res = 10000000;
fill(num,num + 10,-1);
fill(use,use + 10,false);
int index = 0;
getline(cin,s);
for(int i = 0; i < s.size();++i){
if(s[i] != ' ')
{
num[index++] = s[i] - '0';
}
}
solve2(0,index);
//solve(index);
printf("%d\n",res);
}
}
2.Backward Digit Sums
题目大意:按照一定的顺序写下从1到N (1 <= N <= 10)的数字,然后把相邻的数字加起来,得到一个新的列表,其中的数字减少了一个。它们重复这个过程,直到只剩下一个数字。例如:
3 1 2 4
4 3 6
7 9
16
现在我们希望输入 N (代表1,2,…N的数字序列 ) , M
输出字典序列最小的 数字顺序,其中 通过上面的方式,最后一个数为M
思路:直接用STL中的next_permutation获取下一个字典序较大的排列,对每个新得到的排列进行验证,这就是一道水题
代码实现:
#include<cstdio>
#include<algorithm>
using namespace std;
int N,fin;
int num[10];
int update[10]; //用于计算至最后一个值的数组
bool isOk = false;
void printN(){
for(int i = 0; i < N;++i){
if(i == N - 1) printf("%d\n",num[i]);
else printf("%d ",num[i]);
}
}
void solve(int up){
do{
int x = up;
for(int i = 0; i < x;++i){ //初始化
update[i] = num[i];
}
while(x > 0){
if(x == 1){ //只剩一个
if(update[0] == fin){ //肯定是最小符合
printN();
return;
}
}
for(int i = 0;i < x - 1;++i){
update[i] = update[i] + update[i+1];
}
x--;
}
}while(next_permutation(num,num+N)); //得到下一个字典序列最小的
}
int main(){
scanf("%d%d",&N,&fin);
for(int i = 0; i < N;++i){ //初始化 ,不能重复 ,升序开始,这样字典序列对
num[i] = update[i] = i+1;
}
solve(N);
return 0;
}
3.Hopscotch
题目大意:新版跳房子,给你一个5*5的int数组,你可以在任何位置起步,然后选择方向(四个方向),走5步,你走过起点加上以后的5步,总共6个数构成了一组数字序列,求你可以得到的不同的数字序列的数量。
思路:DFS四个方向的迷宫题,但是这个迷宫求解时允许走回头路(大部分迷宫图走回头路会重复计算,所以会开一个数组记录是否走过),但是这里走回头路可能会得到新的解。
对于结果的维护用STL的set,重复序列只插入一次。
代码实现:
#include<cstdio>
#include<set>
using namespace std;
int map[5][5];
int d[4][2] = {{1,0},{-1,0},{0,1},{0,-1}};
set<int> res;
bool isBoard(int x,int y){
if(x >= 5 || x < 0 || y >= 5 || y < 0) return true;
else return false; //一开始写出else false WA了好久····
}
void solve(int x,int y,int step,int aNum){
if(step == 5){ //5步走完,将结果插入set中,有效避免重复结果
res.insert(aNum);
return;
}
for(int i = 0;i < 4;++i)
{
int nx = x + d[i][0],ny = y + d[i][1];
if(!(isBoard(nx,ny))){ //没出界表示可以走
solve(nx,ny,step + 1,aNum * 10 + map[nx][ny]);
}
}
}
int main(){
for(int i = 0;i < 5;++i){
scanf("%d%d%d%d%d",&map[i][0],&map[i][1],&map[i][2],&map[i][3],&map[i][4]);
}
for(int i = 0;i < 5;++i){
for(int j = 0;j < 5;++j)
solve(i,j,0,map[i][j]);
}
printf("%d",res.size());
return 0;
}
二、贪心算法
贪心算法:最基础的算法之一,以某种规则,每次做局部选择时贪心的选择当前最优策略。比如:优先使用面值大的货币····Huffman编码······
需要注意的是:用贪心法做题时,你的贪心策略可能不是整体最优,对于贪心解需要简单证明一下是不是整体最优解,一般使用反证法或者归纳法证明。
以前我一直觉得贪心题是最简单的题了,但是这些习题里的有些贪心题,贪心思路不难但是实现起来细节和分支较多,反而容易出问题。
·区间贪心题:
1.CleaningShifts
题目大意:农夫约翰正在指派他的N 头母牛做一些谷仓周围的清洁杂活。
他总是想让一头奶牛来清理东西,并把一天分成T班,第一班是1班,最后一班是T班。每头奶牛只能在白天的某段时间进行清洁工作。
你的工作是帮助农民约翰分配一些奶牛轮班,这样每个轮班至少有一头奶牛被分配,参与清洁的奶牛尽可能少。如果不能为每个班次分配一头牛,则打印-1。
思路:实际上,问题实质是,输入一系列区间(区间开始,区间结束),这些区间一起覆盖整个时间的最少需要区间数。
首先对区间排序(优先排开始由小到大,其次是结束由大到小),
选取策略:
1.一开始先选最早开始,最晚结束的区间(因为最早开始时间一定要覆盖到),记录当前结束时间nowS = nowE;
2.找到下一个开始值在 nowS前,而结束值最晚的区间,更新nowE(需要注意的是在 nowS前是nowS+1,比如【1,3】+【4,6】可行),直到找到开始值在 nowS后的,此时区间数++,重新更新查找起点nowS = nowE)
3.当nowE >= T,出结果,若找不到返回-1
代码实现:
#include<cstdio>
#include<algorithm>
using namespace std;
#define MAX_N 25005
#define MAX_T 1000100
int N,T;
int need = 0;
struct Cow{
int begin,end;
};
bool comp(const Cow &a,const Cow &b)
{
if(a.begin == b.begin)
return a.end > b.end; //开始一样时,结束的优先
else return a.begin < b.begin;//先开始的优先
}
Cow cows[MAX_N];
void solve(){
int nowS = 0,nowE = 0;
if(cows[0].begin != 1) //最先开始都不从1开始,直接-1
{
need = -1;
return;
}
//选取第一个因为其为[1,MAXend]
nowS = nowE = cows[0].end;
need++;
if(nowE >= T) return;
for(int i = 1;i < N;++i){
if(cows[i].begin > nowS + 1)
//已经找到了下一个开始在 nowS前且结束最晚的,因为已经排过序了
{
need++;
nowS = nowE;
}
if(cows[i].begin <= nowS + 1 && cows[i].end > nowE){
nowE = cows[i].end;
if(nowE >= T) { //找完
need++;
return;
}
}
}
if(nowE < T){
need = -1;
}
}
int main(){
scanf("%d%d",&N,&T);
for(int i = 0;i < N;++i){
scanf("%d %d",&cows[i].begin,&cows[i].end);
}
sort(cows,cows+N,comp);
need = 0;
solve();
printf("%d\n",need);
}
2.Radar Installation
题目大意:假设海岸线是一条无限的直线。海岸的一边是陆地,另一边是海洋。每个小岛都是位于海边的一个点。而任何位于海岸上的雷达装置只能覆盖d的距离(一个半圆)。
海的一面在x轴的上面,陆地的一面在x轴的下面。给定每个岛屿在海里的位置,以及雷达安装覆盖的距离,编写一个程序来找到覆盖所有岛屿的最小数量的雷达安装数。
思路:这个问题也是区间贪心。对于输入的每个岛屿,我们可以计算出每个岛屿为圆心,画半径为d的圆与x轴的交点,这两个交点之间的区间是这个岛屿被覆盖,雷达所可以安装的区间范围。
这样问题就变成了:给定一组区间,尽可能让有交叠的区间合体为同一个区间(减少雷达数)
贪心策略为:
先对区间排序,按区间左边由小到大排。
先取now = 第一个区间右端,然后遍历区间过程中,若当前区间左边小于now,合体两区间,now = 两区间右边最小的那一个(即交叠部分右边),继续遍历,区间左边小于now更新now,直至某区间左边大于now,此时只能安装新的雷达(不能共用了),now也更新为新区间的右边。
代码实现:
#include<cstdio>
#include<algorithm>
#include<Math.h>
using namespace std;
#define MAX_N 1001
int N;
double D;
struct LD{
double l,r;
};
LD lei[MAX_N];
bool cmp(const LD& fir,const LD& sec){
return fir.l < sec.l;
}
int solve(){
//雷达贪心
int res = 1;
double now = lei[0].r;//一开始选择右边的肯定更好 ,now表示当前贪心的雷达x坐标最右范围
for(int i = 1;i < N;++i){
if(lei[i].l <= now){ //满足一雷达多用,此时贪心雷达的最右应该是满足所有区间里右边最小的
now = (now > lei[i].r ? lei[i].r : now);
}
else { //需要加雷达了
res++;
now = lei[i].r;
}
}
return res;
}
int main(){
int now = 1;
while(true){
scanf("%d%lf",&N,&D);
bool simple = false;
if(N == 0.0 && D == 0.0) break;
for(int i = 0;i < N;++i){
double x,y;
scanf("%lf%lf",&x,&y);
if(y > D || y < 0.0){
simple = true;//用来剪枝
}
lei[i].l = x - sqrt(D * D - y * y);
lei[i].r = x + sqrt(D * D - y * y);
}
if(simple){
printf("Case %d: -1\n",now);
now++;
continue;
}
sort(lei,lei+N,cmp);//由大到小
printf("Case %d: %d\n",now,solve());
now++;
}
return 0;
}
3.Stall Reservations
题目大意:
这有一些挑剔的N头奶牛,它们太挑剔了,只有在特定的时间间隔内才能挤奶[A,B]。
显然,FJ必须创建一个预约系统,以确定每头奶牛的挤奶时间可以分配到哪个牛栏。
帮助FJ确定: 为使每头奶牛都能有自己单独的挤奶时间而要求的牛棚的最低数量,同时输出每头牛分配的牛栏序号
思路:实际上,这是一个区间不相交贪心 。为了方便贪心,照例是先排序(开始时间由小到大,再结束时间由小到大)
贪心策略:
1.刚开始,取排序后第一个牛分配第一个牛栏
2.遍历1~N-1号牛,每次检查当前牛的开始时间,是否小于所有已分配牛栏的结束时间(其实上只要跟最小结束时间比):
——如果小于等于,说明当前所有牛栏都没有空闲段分给当前牛,此时新开新的牛栏,结束时间为当前牛的结束时间;
——若大于,说明可以把当前牛分配到最小结束时间的牛栏里,更新该牛栏的结束时间为当前牛插入后的结束时;
PS:所有已分配牛栏的最小结束时间可以用小堆维护
代码实现:
#include<cstdio>
#include<algorithm>
#include<queue>
#define MAX_N 50005
using namespace std;
struct Cow{
int left,right,where,from;//A B 放哪 第几号牛
};
typedef pair<int,int> area; //right quhao
bool cmp1(const Cow& fir,const Cow& sec){
if(fir.left == sec.left)
return fir.right < sec.right;
return fir.left < sec.left;
}
bool cmp2(const Cow& fir,const Cow& sec){
return fir.from < sec.from;
}
Cow cows[MAX_N];
int N;
int solve(){
priority_queue<area,vector<area>,greater<area> > q; //维护最右的最小值
int res = 1;
int nR = cows[0].right;
cows[0].where = res;
q.push(area(nR,res));
for(int i = 1;i < N;++i){
area now = q.top();
if(cows[i].left <= now.first){ //new one 注意:奶牛同一时间交换都不行,要加=
res++;
cows[i].where = res;
q.push(area(cows[i].right,res)); //将新区放入队列
}
else { //此时插入该区右边,需要更新该区右值
q.pop();
cows[i].where = now.second;
q.push(area(cows[i].right,now.second));
}
}
return res;
}
int main(){
scanf("%d",&N);
for(int i = 0;i < N;++i){
int x,y;
scanf("%d%d",&x,&y);
cows[i].left = x;
cows[i].right = y;
cows[i].where = -1;
cows[i].from = i;
}
sort(cows,cows+N,cmp1);
printf("%d\n",solve());
sort(cows,cows+N,cmp2); //排序以后顺序会乱··复原
for(int i = 0;i < N;++i){ //排序以后顺序会乱
printf("%d\n",cows[i].where);
}
}
其他贪心题目:
1.Yogurt factory
题目大意:
有一家酸奶厂生产酸奶。
在接下来的N 周,牛奶的价格和劳动力的成本每周会波动,
C_i (1 <= C_i < = 5000) 是每周生产一单位奶需要成本
Y_i (0 <= Y_i <= 10,000) 是每周要向其客户交付的奶量
Yucky Yogurt拥有一个仓库,可以以固定的费用(1 <= S <= 100)来储存未使用的酸奶,每周每单位酸奶的费用是S (1 <= S <= 100)美分。
幸运的是,酸奶不会变质。Yucky酸奶的仓库是巨大的,所以它可以容纳任意单位的酸奶。
帮助Yucky使其在整个n周期间的成本最小化。
思路:非常简单的贪心题,需要注意的是用int可能结果会溢出,所以用long long 输出格式为%lld
贪心策略:
维护一个历史购买酸奶成本最低的值min_week
从第一周开始,min_week = C_i[0];
之后的每一周比较min_week + S(屯一周增加的成本)和 这一周的C_i[j]的大小关系
——如果min_week + S小,min_week += S(多屯一周成本更新)
——如果C_i[j]小,min_week = C_i[j]
然后min_cost就是不停加上每周的历史购买酸奶成本最低 *每周的需求量
代码实现:
#include <cstdio>
using namespace std;
typedef long long ll;
#define MAX_N 10005
int N,S;//N:每周 S:每周每单位存奶的费用
ll C_i[MAX_N],Y_i[MAX_N];
ll solve(){
ll min_week = C_i[0],min_cost = C_i[0] * Y_i[0];
for(int i = 1; i < N; ++i){
if(min_week + S < C_i[i]){
min_week += S;
}
else {
min_week = C_i[i];
}
min_cost += min_week * Y_i[i];
}
return min_cost;
}
int main()
{
scanf("%d%d",&N,&S);
for(int i = 0; i < N; ++i)
{
scanf("%lld%lld",&C_i[i],&Y_i[i]);
}
printf("%lld\n",solve());
return 0;
}
2.Packets
题目大意:
A工厂生产1* 1, 2* 2, 3* 3, 4* 4, 5* 5, 6* 6等高方包的产品。
这些产品都是用和产品一样高h的方形包裹寄给客户的,尺寸固定为是6*6。
由于费用问题,工厂和客户都应该尽量减少将订单产品从工厂送到客户手中的包裹数量。
思路:从大包袱开始贪心,这题属于思路上简单但是实现起来复杂的那一类
贪心策略:
- 6 *6无法贪心,直接1:1使用
- 5 *5物品有11个格子可供1 *1物体使用
- 4 *4物体有5 *2 *2可用,优先满足2 *2需求再满足1 *1需求
- 3 *3物体优先满足自己4 *3 *3,满足自己之后先满足2 *2在满足 1 *1
- 2 *2物体优先满足自己3 *3 *4,其次满足1 *1
- 若1 *1物品还需要装,直接 (剩余数量+35)/ 36 为所需要袋子
- 将上述所需袋子加起来得到贪心袋子解
代码实现:
#include <cstdio>
using namespace std;
int packets[6];
int solve()
{
int need = packets[5]; //6 * 6
if(packets[4] != 0) // 5 * 5
{
packets[0] = (11 * packets[4] > packets[0] ? 0 : packets[0] - 11 * packets[4]);
need += packets[4];
}
if(packets[3] != 0) // 4 * 4 + 5 * 2 * 2
{
need += packets[3];
if(5 * packets[3] > packets[1]) //2 * 2 放得下
{
packets[0] = (packets[0] - 4 * (packets[3] * 5 - packets[1]) > 0 ? packets[0] - 4 * (packets[3] * 5 - packets[1]) : 0);
packets[1] = 0;
}
else
{
packets[1] = packets[1] - 5 * packets[3];
};
}
if(packets[2] != 0) // 3*3*4
{
need += (packets[2] + 3) / 4;
if(packets[2] % 4 != 0)
{
//当箱子放了i个3*3盒子,剩下的空间最多放j个2*2盒子 其中i={1,2,3} ; j={5,3,1} 由此可得到条件的关系式
if(packets[1] >= 7 - 2 * (packets[2] % 4) )
{
packets[1] -= 7 - 2 * (packets[2] % 4);
packets[0] = (packets[0] - (8 - (packets[2] % 4)) > 0 ? packets[0]-(8-(packets[2] % 4)) : 0);
//当箱子放了i个3*3盒子,并尽可能放了2 * 2,剩下的空间最多放j个1*1盒子 其中i={1,2,3} ; j={7,6,5} 由此可得到条件的关系式
}
else //先把2 * 2 放完;
{
packets[0] = (packets[0] - (36- 9 *(packets[2] % 4) -4*packets[1]) > 0 ? packets[0] - (36- 9 *(packets[2] % 4) -4*packets[1]) : 0);
packets[1] = 0;
}
}
}
if(packets[1] != 0) // 2*2*9
{
{
need += (packets[1] + 8) / 9;
if(packets[1] % 9 != 0)
{
packets[0] = (packets[0] - (36 - 4 * (packets[1] % 9)) > 0 ? packets[0] - (36 - 4 * (packets[1] % 9)) : 0);
packets[1] = 0;
}
}
}
if(packets[0] != 0)
{
need += (packets[0] + 35) / 36;
}
return need;
}
int main()
{
while(true)
{
int a,b,c,d,e,f;
scanf("%d%d%d%d%d%d",&a,&b,&c,&d,&e,&f);
if(a == 0 && b == 0 && c == 0 && d == 0 && e == 0 && f == 0)
break;
packets[0] = a;
packets[1] = b;
packets[2] = c;
packets[3] = d;
packets[4] = e;
packets[5] = f;
printf("%d\n",solve() );
}
}
/* 参考的另一种更简洁的写法
#include<cstdio>
#include<cstring>
int dir[4]={0,5,3,1};
int a[10];
int main()
{
int i,sum,ans;
while(1)
{
sum=0;
for(i=1;i<7;++i)
{
scanf("%d",&a[i]);
sum+=a[i];
}
if(!sum)
break;
ans=a[6]+a[5]+a[4]+(a[3]+3)/4;//计算边长为3 4 5 6的板子消耗量
int cnt_2=a[4]*5+dir[a[3]%4];
if(a[2]>cnt_2)
ans+=(a[2]-cnt_2+8)/9;//当上面剩余的2*2板子量不足时,需要消耗新的板子
int cnt_1=ans*36-a[6]*36-a[5]*25-a[4]*16-a[3]*9-a[2]*4;
if(a[1]>cnt_1)//当上面剩余的1*1板子量不足时,需要消耗新的板子
ans+=(a[1]-cnt_1+35)/36;
printf("%d\n",ans);
}
return 0;
}
*/
3.Allowance
题目大意:
为了奖励奶牛创纪录的产奶量,农场主约翰决定每周给牛一小笔津贴。
FJ有一组N (1 <= N <= 20)不同面额的硬币,每一面值的硬币均匀地划分下一个较大面额的硬币(例如,1分硬币,5分硬币,10分硬币,和50分硬币)。
他用这一套给定的硬币,每周至少付给牛一定数额的钱C(1 <= C <= 100,000,000)。
请帮他计算出付能给牛的最大周数。
思路:
还是要先排序····
贪心策略:
- 面额大于等于C,则直接取它的数目即可。
- 依次从大面额到小面额试着取,如果刚好面额之和 == C,那很好,这个面额可以用小的凑起来,所以选大的合适。
- 如果大的凑不出来相等的,那么选最大的加起来且 < C的,然后从小的开始往大的选,如果能选到的话,就直接选择。
疯狂采取上述策略选择使用到的面额组合,直到没有可以选择的组合出现时停止。
代码实现:
#include <cstdio>
#include <algorithm>
using namespace std;
int N,C;
struct Money
{
int num,worth;
};
bool cmp(const Money& x,const Money& y)
{
return x.worth > y.worth;
}
#define MAX_N 21
Money money[MAX_N];
int use[MAX_N];
int max_week = 0;
void solve()
{
while(true)
{
int now = 0;
bool finds = false;
fill(use,use + N,0);
for(int i = 0; i < N; ++i) // 选取最接近money的组合,从大开始
{
if(money[i].num > 0)
{
int times = ((C - now) / money[i].worth ) < money[i].num ? (C - now) / money[i].worth : money[i].num;
use[i] = times;//如果 times = 0代表C和now很接近,不能放下一张纸币(放了就大了)
now += times * money[i].worth;
if(now == C) //找到了
{
finds = true;
break;
}
}
}
//此时肯定是最接近但小于C的now
if(now < C)
{
for(int i = N-1; i > 0; --i)
{
if(money[i].num - use[i] > 0)
{
//省去while循环,先加到最接近,在补一个
int times = ((C - now) / money[i].worth + 1) < money[i].num ? (C - now) / money[i].worth + 1: money[i].num;
use[i] += times;
now += times * money[i].worth;
if(now >= C)
{
finds=true;
break;
}
}
}
}
if(!finds)
break;
int minn = 100000000;
for(int k = 0; k < N; k++) //记录最小的支付数
if(use[k] > 0)
minn = minn < money[k].num / use[k] ? minn : money[k].num / use[k];
max_week += minn;
for(int j = 0; j < N; j++) //从硬币总数量中减去支付数
{
if(use[j] > 0 && money[j].num > 0)
{
money[j].num -= minn * use[j];
}
}
}
printf("%d\n",max_week);
}
int main()
{
scanf("%d%d",&N,&C);
for(int i = 0; i < N; ++i)
{
scanf("%d%d",&money[i].worth,&money[i].num);
if(money[i].worth >= C)
{
max_week += money[i].num;
money[i].num = 0;
}
}
sort(money,money+N,cmp);
solve();
return 0;
}
4.Stripies
题目大意:
我们的化学生物学家发明了一种新的非常有用的生命形式,叫做Stripies
这些Stripies是透明的、无定形的变形虫。当其中两个碰撞,一个新的Stripies出现代替他们。已知,当两个重量为m1和m2的条纹相撞时,产生的新Stripies的重量等于2* sqrt(m1* m2)。
写一个程序来输出对一个给定的Stripies群体,什么样的碰撞可以最大幅度地减少总重量。
思路:
贪心策略:
对一群数字 a b c d… n
目标是求 2 * sqrt( …2 * sqrt(… 2 * sqrt( x,y)…)…) 的最小值
很明显可以提出所有的2 ,越里面的(越先取出用来碰撞的数),其sqrt次数越多,所以为了让值越小(减少重量越多),我们希望让越大的值越早被取出!
而不是—— 每次取最远的两个数(m1+m2 -2 sqrt(m1* m2) == 【sqrt(m1)-sqrt(m2)】^2,)
~ 因为这种取法可能局部最优,但整体不优化…
代码实现:
#include<cstdio>
#include<algorithm>
#include<Math.h>
using namespace std;
#define MAX_N 105
double Stripies[MAX_N];
int N;
double solve(){
if(N == 1){
return Stripies[0];
}
int nN = N - 1;
while(nN > 0){
double temp = 2 * sqrt(Stripies[nN] * Stripies[nN - 1]); //让越大的值越早被取出
Stripies[nN - 1] = temp;
nN -= 1;
}
return Stripies[0];
}
int main(){
scanf("%d",&N);
for(int i = 0 ; i < N ;++i){
scanf("%lf",&Stripies[i]);
}
sort(Stripies,Stripies + N); //从小到大
printf("%.3f\n",solve());
return 0;
}
5.Protecting the Flowers
题目大意:
约翰去砍柴,留下N头牛照常吃草。当他回来时,他发现那群奶牛正在吃他花园的花。为了尽量减少后续的损失,FJ决定立即采取行动,将每头牛运回自己的谷仓。
每头牛i都在离它自己的谷仓有Ti分钟的地方。在等待运输时,每分钟销Di花。
FJ一次只能把一头牛运回她的牲口棚。把奶牛i移到它的谷仓需要2 * Ti分钟(Ti到那里和Ti返回)。
FJ从花圃开始,把奶牛送到谷仓,然后走回花圃,没有花额外的时间去找下一头需要运输的奶牛。
写一个程序,以确定顺序,在FJ应该捡起的奶牛,使鲜花的总数被破坏最小化。
思路:
贪心策略:
贪心:每次局部最优实际上就是整体最优~
每次选择都会进行任意两头牛先运哪头的判断:
两个牛中 A,B, 属性分别为分别为eatA,timeA, eatB,timeB
选A的时候损失 timeAeatB — > timeA / eatA
选B的时候损失 timeBeatA — > timeB / eatB
(双方同除以eatA*eatB)
令time/eat为一个牛的比率x,我们贪心地选取两头牛中x小的,其损失小。
可以证明,每次当前所有剩余牛中x最小的那个为局部最优解 (X越小选择它的损失越小)
代码实现:
#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll;
#define MAX_N 1000005
struct Cow{
int eat,time;
};
int N;
Cow cows[MAX_N];
bool cmp (const Cow& x,const Cow& y){
//return x.time / x.eat < y.time / y.eat; 注意是int相除,2/5 和 1/6 都是0
return x.time * y.eat < y.time * x.eat ;
}
ll allE = 0;
ll solve(){
ll res = 0;
for(int i = 0;i < N;++i){
res += cows[i].time * (allE - cows[i].eat);
//printf("\n%d,%d,%d",cows[i].time,allE - cows[i].eat, cows[i].time * (allE - cows[i].eat) );
allE -= cows[i].eat;
}
return res * 2;
}
int main(){
scanf("%d",&N);
for(int i = 0 ;i < N ;++i){
scanf("%d%d",&cows[i].time,&cows[i].eat);
allE += cows[i].eat;
}
sort(cows,cows+N,cmp);
printf("%lld\n",solve()); //这里弄错了 longlong 用 %lld来输出
}
以前还觉得贪心算法多简单啊,但是实际写下来还是容易写错,在各种细节选择和分支判断上出现差错·······
之后再抽时间补充一下剩余 de 练习题吧,纸上谈兵还是不太行