7.7 竞赛题目选讲

题目可能有些难,请阅读完前面的篇章后,选择是否进行阅读。


7-11 宝箱 (UVA 12325)

你有一个体积为N的箱子和两种数量无限的宝物。宝物1的体积为S1,价值为V1;宝物1的体积为S2,价值为V2。你的任务就是计算出最多能装多大价值的宝物。其中每种宝物都必须拿非负整数个。

分析:这个问题看起来比较简单,直接枚举宝物1或宝物2的个数,然后尽可能地多拿宝物2或宝物1。但这个时候可能出现一个问题,当S1和S2的值太过于小了,这种做法的复杂度会比较高,代码如下:

#include<iostream>
using namespace std;
int main(){//cost表示某次的价值,cost_m表示最高的价值 long long N,S1,V1,S2,V2,cost,cost_m=0; cin>>N>>S1>>V1>>S2>>V2;for (int i=0;i<=N/S1;i++) {cost=i*V1+(N-i*S1)/S2*V2; if(cost_m<cost) cost_m=cost;}cout<<cost_m; return 0;
}

于是在这里我们只需要一些优化:

if (S1*S2<=N) {//S2*V1>S1*V2时,宝物1单位体积的价值更高,全选择宝物1 if (S2*V1>S1*V2) {cost=(N/(S1*S2))*S2*V1;} else{cost=(N/(S1*S2))*S1*V2;} N=N%(S1*S2);}

如果N的值相较于S1和S2的值过大,我们可以将N转化为N对S1*S2的余数。在a*S1*S2(a为N/S1*S2)的体积中,我们可以直接根据单位体积的价格大小选择全部买宝物1和宝物2,因为除以S1和S2都没有余数,所以不用一遍一遍地枚举(大家仔细想一下为什么)。

循环这里最好也优化一下:

//比较S1和S2的大小,选用大一点的S1或S2能够减少枚举的次数 if (S1>S2) for (int i=0;i<=N/S1;i++) {cost+=i*V1+(N-i*S1)/S2*V2; if(cost_m<cost) cost_m=cost; cost-=i*V1+(N-i*S1)/S2*V2;}else for (int i=0;i<=N/S2;i++){cost+=i*V2+(N-i*S2)/S1*V1; if(cost_m<cost) cost_m=cost; cost-=i*V2+(N-i*S2)/S1*V1;}

完整代码如下:

#include<iostream>
using namespace std;
int main(){//cost表示某次的价值,cost_m表示最高的价值 long long N,S1,V1,S2,V2,cost,cost_m=0; cin>>N>>S1>>V1>>S2>>V2;//如果S1乘以S2的值小于等于N,此时S1和S2的值相对于N肯定是比较小了//这个时候我们可以选取N中S1*S2的体积按照单位体积的价格选择宝物1或者宝物2 if (S1*S2<=N) {//S2*V1>S1*V2时,宝物1单位体积的价值更高,全选择宝物1 if (S2*V1>S1*V2) {cost=(N/(S1*S2))*S2*V1;} else{cost=(N/(S1*S2))*S1*V2;} N=N%(S1*S2);}//比较S1和S2的大小,选用大一点的S1或S2能够减少枚举的次数 if (S1>S2) for (int i=0;i<=N/S1;i++) {cost+=i*V1+(N-i*S1)/S2*V2; if(cost_m<cost) cost_m=cost; cost-=i*V1+(N-i*S1)/S2*V2;}else for (int i=0;i<=N/S2;i++){cost+=i*V2+(N-i*S2)/S1*V1; if(cost_m<cost) cost_m=cost; cost-=i*V2+(N-i*S2)/S1*V1;}cout<<cost_m; return 0;
}

这个代码写的比较快,大家可以帮我检查一下代码有没有出什么问题,或者还有什么细节没有考虑到的。


7-12 旋转游戏 (UVA 1343)

这个棋盘我打印不出来,大家直接看题面吧

分析:就是一种形状为井字形的棋盘(注意是形状上类似于,从操作上看更加像四个由字符组成的环形卡在了一起),A-H一共代表八种分别运动这四个环形的不同方向。比如我们进行A操作,就代表A所在的那个环向A方向进行旋转。我们的任务是得到旋转次数最少的,如果旋转次数有多解,选择操作序列字符最最小的方案。

首先一共有24个点,8个1,8个2,8个3。所以显然不能使用常规的方法将状态给存储起来(8个1,8个2,8个3的全排列高达90亿)。

还记得倒水那个问题减少状态结点个数的方式么?那道题我们只需要通过第一杯水和第二杯水的量来计算出第三杯水的量。所以这道题我们可以使用同样的方式去减少状态结点的个数,由于我们只需要判断中间八个数字是否全部相等就可以了,所以当我们判断是否全为1时,点的值为2或3没有任何区别。也就是说状态结点总数可以转化为8个a(a表示当前判断相等的数)以及16个非a数字的全排列个数。

这道题,我看书上写的是除了BFS外还可以使用IDA*。但是说实话我并不觉得运算的效率会高很多,但是还是带着大家来一起来编写一下(我真的没觉得快到哪里去,感觉很多地方的使用感觉就是为了使用这个算法而使用,而并没有特别大的意义)。

大致框架如此:

#include<iostream>
#include<cstring>
using namespace std;
const int N=24,LEN=8;
int dir[LEN][LEN-1]={//dir表示A-H不同方向行和列的点在数组a[i]的下标 {0,2,6,11,15,20,22},{1,3,8,12,17,21,23},{10,9,8,7,6,5,4},//A-C方向 {19,18,17,16,15,14,13},{23,21,17,12,8,3,1},{22,20,15,11,6,2,0},//D-F方向{13,14,15,16,17,18,19},{4,5,6,7,8,9,10}//G,H方向
};//check_order表示需要判断是否相等的8个点的坐标
int check_order[LEN]={6,7,8,11,12,15,16,17},a[N],maxd;
int main(){for (int i=0;i<24;i++) cin>>a[i];for (maxd=1;;maxd++) if(dfs(0)) break;//迭代加深主体
}

用于扩展结点的“旋转函数”:

void rotate(int dir_s){//旋转函数,dir_s表示旋转环形的方向 int t=a[dir[dir_s][0]]; for(int i=1;i<LEN-1;i++) a[dir[dir_s][i-1]]=a[dir[dir_s][i]];a[dir[dir_s][LEN-2]]=t;
}

启发函数:

//统计中间8个数中最少有几个需要调整的,同时也是启发函数,也用于判断搜索什么时候结束
int count_unordered(){ int n[3]={0};//n[i]表示8个数字中i+1的个数 for( int i=0;i<LEN;i++) n[a[check_order[i]]-1]++; return LEN-max(max(n[0],n[1]),n[2]);
}

dfs函数(我真的觉得真的要做的话,可能bfs在效率上不比IDA*差):

bool dfs(int d){//中间八个数不需要调整,dfs结束int num=count_unordered(); if(!num) return true; if(d+num>maxd) return false;//d+num就是启发函数,我真的感觉没有简化很多 int temp[N]; memcpy(temp,a,sizeof(a));//进行结点的扩展(8个方向的旋转) for(int i=0;i<LEN;i++){ rotate(i); op[d]=i+'A';if(dfs(d+1)) return true; memcpy(a,temp,sizeof(temp));} return false;
}

完整代码如下:

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=24,LEN=8;
int dir[LEN][LEN-1]={//dir表示A-H不同方向行和列的点在数组a[i]的下标 {0,2,6,11,15,20,22},{1,3,8,12,17,21,23},{10,9,8,7,6,5,4},//A-C方向 {19,18,17,16,15,14,13},{23,21,17,12,8,3,1},{22,20,15,11,6,2,0},//D-F方向{13,14,15,16,17,18,19},{4,5,6,7,8,9,10}//G,H方向
};//check_order表示需要判断是否相等的8个点的坐标
int check_order[LEN]={6,7,8,11,12,15,16,17},a[N],maxd;
char op[50];//op表示各个步骤旋转方向的字符
void rotate(int dir_s){//旋转函数,dir_s表示旋转环形的方向 int t=a[dir[dir_s][0]]; for(int i=1;i<LEN-1;i++) a[dir[dir_s][i-1]]=a[dir[dir_s][i]];a[dir[dir_s][LEN-2]]=t;
}//统计中间8个数中最少有几个需要调整的,同时也是启发函数,也用于判断搜索什么时候结束
int count_unordered(){ int n[3]={0};//n[i]表示8个数字中i+1的个数 for(int i=0;i<LEN;i++) n[a[check_order[i]]-1]++; return LEN-max(max(n[0],n[1]),n[2]);
}
bool dfs(int d){//中间八个数不需要调整,dfs结束int num=count_unordered(); if(!num) return true; if(d+num>maxd) return false;//d+num就是启发函数,我真的感觉没有简化很多 int temp[N]; memcpy(temp,a,sizeof(a));//进行结点的扩展(8个方向的旋转) for(int i=0;i<LEN;i++){ rotate(i); op[d]=i+'A';if(dfs(d+1)) return true; memcpy(a,temp,sizeof(temp));} return false;
}
int main(){for (int i=0;i<24;i++) cin>>a[i];for (maxd=1;;maxd++) if(dfs(0)) break;//迭代加深主体for( int i=0;i<maxd;i++) printf("%c",op[i]);
}

7-13 快速幂计算 (UVA 1374)

输入正整数n,问最少需要几次乘除法可以从x得到xn?例如x31需要6次:x2=x*x,x4=x2*x2,x8=x4*x4,x16=x8*x8,x32=x16*x16,x32=x32\x。计算过程中x的指数应该总是正整数。

分析:快速幂的算法部分,我已经在之前的文章中进行过一个较为详细地介绍了。

本质上这个问题的解决类似于我们之前做过的埃及分数,就是说(扩展结点的个数依旧可能不确定),解决方法的话依旧是IDA*。当前状态是已经得到的指数集合,操作是任选两个数进行加法和减法,并且不能产生重复的数。

那么这里的估价函数(剪枝)是什么样的呢?d表示当前深度,maxd表示深度上限,now表示当前指数集合中的最大值。如果now*2max-d之后仍小于n,则剪枝(因为无论怎么样都到不了了).

此外,不应该任选两个数,而是选择那个最大的数,对它进行加法或者减法来扩展结点(这个做法叫做结点排序)。

代码实现如下:

#include<cstdio>
int n,maxd,A[35];
bool dfs(int d,int now){//d>maxd是迭代加深搜索结束的表示,now小于等于0是题目的限定 if (d>maxd||now<=0||now<<(maxd-d)<n) return false;//now<<(maxd-d)<n就可以理解为估价函数 if (now==n||now<<(maxd-d)==n) return true; A[d]=now; for (int i=0;i<=d;++i){//扩展结点 if (dfs(d+1,now+A[i])) return true; if (dfs(d+1,now-A[i])) return true;} return false;
}
int main(){scanf("%d",&n); for (maxd=0;!dfs(0,1);maxd++); printf("%d",maxd); return 0;
}

7-14 网格动物 (UVA 1602)

输入n,w,h,求能放在w*h(1<=n<=10,1<=w,h<=n),求能放在w*h网格里的不同n连块的个数(注意,平移,旋转,翻转后相同的算作同一种)。

分析:分析个屁,不会,一点思路都没有


7-15 破坏正方形 (UVA 1603)

心力交瘁,自己看题面吧

分析:就是给你一个残缺的火柴棍组成的正方形网格,然后需要你得到在剩下的火柴棒钟,至少还要拿走多少根火柴棒才能破坏掉所有的正方形(就是剩下的火柴棒不组成正方形啦)。

书上提供了两种做法,第一种做法是每次考虑一个没有被破坏的正方形,然后在边界上找一根火柴棒拿掉。从小正方形考虑到大正方形,因为破坏完小正方形后,很多大正方形就已经被破坏了。

首先是dfs前构建正方形到正方形所含边数的准备:

void init(){//dfs前的准备 num=0; maxd=n*n; memset(s_sides,0,sizeof(s_sides));//size表示正方形的边长 for (int size=1;size<=n;size++){ for(int i=1;i<=n-size+1;i++)for(int j=1;j<=n-size+1;j++){//(i,j)表示边长为size的某个正方形左上角的坐标 num++; s_full[num]=4*size; s_now[num]=0;//一个正方形所含的火柴数肯定是边长*4for (int k=0;k<size;k++){//初始化s_sides数组,表示每个正方形中有哪些火柴棒//a和b分别表示第一行和最后一行第k个火柴的编号,c和d分别表示第一列和最后一行第k个火柴的编号 //对于行而言,每一行包括横着的n根火柴和竖着的n+1根火柴棒,j+k就是第几列//对于列而言,就将整个正方形旋转90度然后再按照行计算就能够明白了   int a=(i-1)*(2*n+1)+j+k,b=(i+size-1)*(2*n+1)+j+k;int c=n*(2*(i+k)-1)+i+k+j-1,d=n*(2*(i+k)-1)+i+k+j+size-1;s_sides[num][a]=1; s_sides[num][b]=1; s_sides[num][c]=1; s_sides[num][d]=1; s_now[num]+=(is_existed[a]+is_existed[b]+is_existed[c]+is_existed[d]); } }}
}

dfs函数(讲道理书上说的是要用IDA*做,但是我真的没想明白这道题怎么用IDA*去做,我甚至最开始时连迭代加深搜索的框架都没能看出来,我个人感觉就是这个算法就是一个普通的回溯法):

//get_s函数表示得到当前需要扩展的下一个结点(需要破坏的下一个正方形编号)
int get_s(){for (int i=1;i<=num;i++) if (s_full[i]==s_now[i]) return i; return 0;}
void dfs(int d){//书上说要用迭代加深搜索算法,但这里很明显用的是简单的回溯法 if (d==maxd) return;//理论上应该完全没有这样的可能,只是为了结束dfs使用的操作   if (!get_s()) {maxd=d; return;}//表示已经处理完了,dfs结束    for (int i=1;i<=2*n*(n+1);i++){ if (s_sides[get_s()][i]){//找当前最小的正方形所含的某个边进行破坏 //由于找到的这个正方形一定是完整的,没被破坏的正方形,所以一定不会出现某个边不存在的情况 for (int j=1;j<=num;j++) if (s_sides[j][i]) s_now[j]--; dfs(d+1); for (int j=1;j<=num;j++) if (s_sides[j][i]) s_now[j]++;}}
}

完整代码如下(跑的时间还是很长的):

#include<iostream>
#include<cstring>
using namespace std;
int n,k,x,num,maxd,is_existed[100],s_sides[100][100],s_now[100],s_full[100];
//num表示正方形编号,s_now[i],s_full[i]表示第i个正方形目前拥有的火柴数和应该拥有的火柴数目
//is_existed[i]表示编号为i的火柴是否存在,s_sides[i][j]表示编号为j的火柴在编号为i的正方形中是否存在
void init(){//dfs前的准备 num=0; maxd=n*n; memset(s_sides,0,sizeof(s_sides));//size表示正方形的边长 for (int size=1;size<=n;size++){ for(int i=1;i<=n-size+1;i++)for(int j=1;j<=n-size+1;j++){//(i,j)表示边长为size的某个正方形左上角的坐标 num++; s_full[num]=4*size; s_now[num]=0;//一个正方形所含的火柴数肯定是边长*4for (int k=0;k<size;k++){//初始化s_sides数组,表示每个正方形中有哪些火柴棒//a和b分别表示第一行和最后一行第k个火柴的编号,c和d分别表示第一列和最后一行第k个火柴的编号 //对于行而言,每一行包括横着的n根火柴和竖着的n+1根火柴棒,j+k就是第几列//对于列而言,就将整个正方形旋转90度然后再按照行计算就能够明白了  int a=(i-1)*(2*n+1)+j+k,b=(i+size-1)*(2*n+1)+j+k;int c=n*(2*(i+k)-1)+i+k+j-1,d=n*(2*(i+k)-1)+i+k+j+size-1;s_sides[num][a]=1; s_sides[num][b]=1; s_sides[num][c]=1; s_sides[num][d]=1; s_now[num]+=(is_existed[a]+is_existed[b]+is_existed[c]+is_existed[d]); } }}
}//get_s函数表示得到当前需要扩展的下一个结点(需要破坏的下一个正方形编号)
int get_s(){for (int i=1;i<=num;i++) if (s_full[i]==s_now[i]) return i; return 0;}
void dfs(int d){//书上说要用迭代加深搜索算法,但这里很明显用的是简单的回溯法 if (d==maxd) return;//理论上应该完全没有这样的可能,只是为了结束dfs使用的操作   if (!get_s()) {maxd=d; return;}//表示已经处理完了,dfs结束    for (int i=1;i<=2*n*(n+1);i++){ if (s_sides[get_s()][i]){//找当前最小的正方形所含的某个边进行破坏 //由于找到的这个正方形一定是完整的,没被破坏的正方形,所以一定不会出现某个边不存在的情况 for (int j=1;j<=num;j++) if (s_sides[j][i]) s_now[j]--; dfs(d+1); for (int j=1;j<=num;j++) if (s_sides[j][i]) s_now[j]++;}}
}
int main(){cin>>n>>k; for (int i=1;i<=2*n*(n+1);i++) is_existed[i]=1;for (int i=0;i<k;i++) {cin>>x; is_existed[x]=0;}init(); dfs(0); cout<<maxd; return 0;
}

另外有一种比较好的方法需要用DLX算法,由于并不属于本章节的内容,于是直接放弃(应该是下本书的内容),至于搜索火柴棒的那个方法(完全没有看到有人这么做呢)。


总结

本章学习了以下一些内容:

直接枚举:看了例题都知道很简单

枚举子集和排列:子集的话,如果数据比较小就用二进制,数据比较大的话就用递归法。全排列,直接STL吧hhhhhh。

回溯法:在递归枚举(dfs)的基础上增加一条:违反要求时及时终止(及时止损)。

状态空间搜索:又称“隐式图搜索”或者“产生式系统”。反正我感觉就是一个普通的BFS加上一个判重的系统和表。其中A*算法(估价函数)可以很有效地提高我们原本算法的效率。

迭代加深搜索(IDDFS)和IDA*:总结?我到现在都不知道IDA*比起A*的优势到底在哪里,我给你总结个鬼哦。

7.7 竞赛题目选讲相关推荐

  1. 4.4 竞赛题目选讲

    竞赛题目选讲 这里的题目可能和大家在做的实验项目有些不太一样,希望大家根据自己的需要阅读本章节. 4-2 刽子手游戏 (UVA 489) 书上的题面少了一些很重要的东西,真正的题面请点开这里 分析:根 ...

  2. 8.6 竞赛题目选讲

    8.6 竞赛题目选讲 量力而行 8-10 抄书 (UVA 714) 把一个包含m个正整数的划分成k个(1<=k<=m<=500)非空的连续子序列,使得每个正整数恰好属于一个序列.设第 ...

  3. 红书《题目与解读》第一章 数学 题解《ACM国际大学生程序设计竞赛题目与解读》

    整理的算法模板合集: ACM模板 点我看算法全家桶系列!!! 实际上是一个全新的精炼模板整合计划 红书<题目与解读>第一章 数学 题解<ACM国际大学生程序设计竞赛题目与解读> ...

  4. 【牛客每日一题】tokitsukaze and Soldier 题目精讲 贪心、优先队列、堆

    链接:https://ac.nowcoder.com/acm/problem/50439 来源:牛客网 ACM在线模板 今天才发现牛客推出了一个每日一题的版块,3月25号就开始了,今天才发现,赶紧补救 ...

  5. 【每日一题】8月28日题目精讲 编号

    [每日一题]8月28日题目精讲 编号 链接:https://ac.nowcoder.com/acm/problem/19925 来源:牛客网 题目描述 你需要给一批商品编号,其中每个编号都是一个7位1 ...

  6. 【每日一题】7月17日题目精讲—BOWL 碗的叠放

    [每日一题]7月17日题目精讲-BOWL 碗的叠放 时间限制:C/C++ 1秒,其他语言2秒 空间限制:C/C++ 262144K,其他语言524288K 64bit IO Format: %lld ...

  7. 【每日一题】7月13日题目精讲—Kingdom

    [每日一题]7月13日题目精讲-Kingdom 文章目录 题目描述 题解: 代码: 时间限制:C/C++ 2秒,其他语言4秒 空间限制:C/C++ 1048576K,其他语言2097152K 64bi ...

  8. 练习图200例图纸讲解_【宅家数学课23】经典微课6:苏教版六年级下册比例尺典型例题选讲及练习(含答案)...

    (截止日期:3月31日) 学习过程 1.点击观看经典微课: 微课视频 <比例尺> 2.认真学习典型例题,完成下方练习题 3.查看答案,在家长指导下批改,订正错误. 苏教版小学数学六年级下册 ...

  9. 20190509杂题选讲

    这次杂题选讲好多思维题神仙题啊= =顺便学了波线段树上二分= = Normal 题目大意戳这 CF1083C CDW讲的神仙题*1 题解戳这 AGC002E 我讲的题,是个人写的程序都比我写的程序跑得 ...

最新文章

  1. mysql的字段空格是null_MySQL中NULL与空字符串 空格问题
  2. 天马行空W:在C++中调用DLL中的函数
  3. Ubuntu13.10下编译安装opencv2.4.9
  4. 【2017级面向对象程序设计】作业一
  5. java服务器端编程
  6. Android功耗(17)---省电续航-AAL 屏幕内容省电
  7. 数字轮廓投影仪行业调研报告 - 市场现状分析与发展前景预测
  8. dijkstra 算法_路径规划算法总结
  9. Android Fragment手柄后退按钮按下[重复]
  10. MyBatis→SqlSession、sqlMapConfig.xml、映射XML文件、OGNL、拼接SQL标签、取值查值、批量SQL、一对多多对一多对多
  11. King Arthur
  12. 内存分配——栈、堆、静态区、符号区等等
  13. 我的web前端工作日志14------2020年度总结
  14. javascript用DOM解释XML
  15. 交换机基础原理,冲突域和广播域
  16. sinc函数原型滤波器窗口matlab,sinc函数
  17. [BZOJ2959] 长跑
  18. NOIP2017普及组复赛——T4跳房子
  19. 最全HTTP协议详解
  20. 软件测试每日一题—分享功能测试

热门文章

  1. 字体的样式及字体的分类
  2. 清华大学数学与计算机学院院长,清华“冰冰”,今日亮相!
  3. 冰冰学习笔记:异常处理
  4. linux下的常用命令 + 环境配置 + 数据库安装 一步到位!
  5. Process Explorer使用图文教程
  6. 几种主流编程语言的优势与不足
  7. Bing必应地图中国API入门讲座之八:显示驾车路线
  8. python的try和except用法_Python中的错误和异常处理简单操作示例【try-except用法】...
  9. 收集回顾 SharePoint 历史版本比较 SharePoint Server 2016
  10. 虾米音乐API破解,python模拟接口