7.7 竞赛题目选讲

7.7 竞赛题目选讲

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


7-11 宝箱 (UVA 12325)

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

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

#include
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
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
#include
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
#include
#include
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
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)
    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;//一个正方形所含的火柴数肯定是边长*4
			for (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
#include
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;//一个正方形所含的火柴数肯定是边长*4
			for (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*的优势到底在哪里,我给你总结个鬼哦。

你可能感兴趣的:(算法学习,算法,c++,数据结构)