NOIP学习小站
西安交通大学附属中学航天学校

循环枚举——P2089 烤鸡

枚举算法是我们在日常中使用到的较多的一个算法,它简单粗暴,算法的核心是暴力枚举所有可能情况,并进一步从中筛选出满足条件的方案。因为枚举法编程实现最简单,并且得到的结果往往是正确的,所以虽然枚举算法非常暴力,而且速度可能很慢(导致超时),但却是我们编程以及竞赛时应该甚至是优先考虑的解决方案,特别是没有更好的优化方案的时候。当然如果能够将暴力枚举算法更好地优化,也完全有可能在设定的时间内求出正解。

本章节将依次介绍暴力枚举的三种策略:循环枚举子集枚举排列枚举

循环枚举的策略是借助多重循环结构枚举所有可能的情况,是最简单的暴力枚举方法。和模拟算法策略一样,循环枚举也没有固定的做法,这里也通过例题来介绍循环枚举策略。但是需要注意的是,对于同一个问题,循环枚举的做法往往不止一种,其中有效率高运行速度快的,也有算法原始效率低下的,这也往往会导致超时。

因此在使用循环枚举暴力求解时也要想办法尽可能提高算法的效率。最简单的策略当然是减少多重循环的嵌套层级,例如原始的循环枚举算法需要三重循环,如果可以优化改进成双重循环甚至简单循环就能实现,那么程序效率会大大提升。也可以从减少循环次数的角度出发提高枚举效率,特别是越外层的循环,如果能减少循环次数,枚举效率会得到明显提升。

来看例题:P2089 烤鸡

方案:10种配料,每种配料可以放1~3克,问题要求的就是所有配料的质量之和为\(n\)的所有方案。最简答的做法当然是暴力循环枚举,使用一个10层循环来枚举每种配料的质量,因为每层循环都只枚举3个数,所以最内层循环执行的次数为\(3^{10}\),虽然循环嵌套层次很大,但是执行次数在可控范围内,不会超时。实际操作时也可以只使用九重循环嵌套来暴力枚举,因为枚举了9种配料,最后一种配料可以直接计算出来。

#include<cstdio>
int main(){
	int n,tot=0;
	scanf("%d",&n);
	//特判
	if(n>30 || n<10){
		printf("0\n");	
		return 0;
	}
	//第一次暴力枚举,计算方案数
	for(int i1=1;i1<=3;i1++){
		for(int i2=1;i2<=3;i2++){
			for(int i3=1;i3<=3;i3++){
				for(int i4=1;i4<=3;i4++){
					for(int i5=1;i5<=3;i5++){
						for(int i6=1;i6<=3;i6++){
							for(int i7=1;i7<=3;i7++){
								for(int i8=1;i8<=3;i8++){
									for(int i9=1;i9<=3;i9++){
										int x = n-i1-i2-i3-i4-i5-i6-i7-i8-i9;
										if(x>=1 && x<=3) tot++; 
									}
								}
							}
						}
					}
				}
			}
		}
	}
	printf("%d\n",tot);
	//第二次暴力枚举,输出具体方案
	for(int i1=1;i1<=3;i1++){
		for(int i2=1;i2<=3;i2++){
			for(int i3=1;i3<=3;i3++){
				for(int i4=1;i4<=3;i4++){
					for(int i5=1;i5<=3;i5++){
						for(int i6=1;i6<=3;i6++){
							for(int i7=1;i7<=3;i7++){
								for(int i8=1;i8<=3;i8++){
									for(int i9=1;i9<=3;i9++){
										int x = n-i1-i2-i3-i4-i5-i6-i7-i8-i9;
										if(x>=1 && x<=3){
											printf("%d %d %d %d %d %d %d %d %d %d\n",i1,i2,i3,i4,i5,i6,i7,i8,i9,x);
										}
									}
								}
							}
						}
					}
				}
			}
		}
	}
	return 0;
}

很显然,上面的程序出现了重复的计算,我们可以想办法在最内层循环中使用数组记录下方案配料情况,这样只需要一次暴力枚举即可,这是典型的空间换时间策略(下面的程序去掉了一些不必要的括号):

#include<cstdio>
//存储多个字符串的二维char数组 
char s[20000][25];
int main(){
	int n,tot=0;
	scanf("%d",&n);
	//特判 
	if(n>30 || n<10){
		printf("0\n");	
		return 0;
	}
	//暴力枚举,记录方案 
	for(int i1=1;i1<=3;i1++)
		for(int i2=1;i2<=3;i2++)
			for(int i3=1;i3<=3;i3++)
				for(int i4=1;i4<=3;i4++)
					for(int i5=1;i5<=3;i5++)
						for(int i6=1;i6<=3;i6++)
							for(int i7=1;i7<=3;i7++)
								for(int i8=1;i8<=3;i8++)
									for(int i9=1;i9<=3;i9++){
										int x = n-i1-i2-i3-i4-i5-i6-i7-i8-i9;
										if(x>=1 && x<=3){
											//使用sprintf输出到字符串中存储 
											sprintf(s[tot++],"%d %d %d %d %d %d %d %d %d %d\0",i1,i2,i3,i4,i5,i6,i7,i8,i9,x);
										}
									}
	printf("%d\n",tot);
	for(int i=0;i<tot;i++){
		printf("%s\n",s[i]);
	}
	return 0;
}
#include<cstdio>
//存储多个字符串的二维char数组 
char s[20000][25];
int main(){
    int n,tot=0;
    scanf("%d",&n);
    //特判 
    if(n>30 || n<10){
        printf("0\n");    
        return 0;
    }
    //暴力枚举,记录方案
    for(int i1=1;i1<=3;i1++){
        for(int i2=1;i2<=3 && i1+i2<n;i2++){
            for(int i3=1;i3<=3 && i1+i2+i3<n;i3++){
                for(int i4=1;i4<=3 && i1+i2+i3+i4<n;i4++){
                    for(int i5=1;i5<=3 && i1+i2+i3+i4+i5<n;i5++){
                        for(int i6=1;i6<=3 && i1+i2+i3+i4+i5+i6<n;i6++){
                            for(int i7=1;i7<=3 && i1+i2+i3+i4+i5+i6+i7<n;i7++){
                                for(int i8=1;i8<=3 && i1+i2+i3+i4+i5+i6+i7+i8<n;i8++){
                                    for(int i9=1;i9<=3 && i1+i2+i3+i4+i5+i6+i7+i8+i9<n;i9++){
                                        int x = n-i1-i2-i3-i4-i5-i6-i7-i8-i9;
                                        if(x<=3){
                                            //使用sprintf输出到字符串中 
                                            sprintf(s[tot++],"%d %d %d %d %d %d %d %d %d %d\0",i1,i2,i3,i4,i5,i6,i7,i8,i9,x);
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    printf("%d\n",tot);
    for(int i=0;i<tot;i++){
        printf("%s\n",s[i]);
    }
    return 0;
}

上面的程序数组究竟开多大呢?假设内层循环条件都满足,那么会有\(3^9\)(19683)种方案存储到数组中,所以可以开一个20000行的二维数组来存储方案(实际远远用不到这么多)。甚至可以专门编写一个程序通过打擂台来计算10~30的每一个数作为输入时的最多方案数,来作为开数组时数组的行数,这也是竞赛时的一些小技巧。

优化方案:上面的暴力枚举其实可以进一步优化的,例如i1+i2+i3+i4就已经超过\(n\)了,那么完全没有必要再进一步枚举剩下的配料了,可以使用break语句来避免这样的情况(也可以修改每层循环的条件),这样做限定了循环枚举的范围,当然能提高程序的执行效率:

#include<cstdio>
//存储多个字符串的二维char数组 
char s[20000][25];
int main(){
	int n,tot=0;
	scanf("%d",&n);
	//特判 
	if(n>30 || n<10){
		printf("0\n");	
		return 0;
	}
	//暴力枚举,记录方案
	for(int i1=1;i1<=3;i1++){
		for(int i2=1;i2<=3;i2++){
			if(i1+i2>=n) break;     //可以不要
			for(int i3=1;i3<=3;i3++){
				if(i1+i2+i3>=n) break;     //可以不要
				for(int i4=1;i4<=3;i4++){
					if(i1+i2+i3+i4>=n) break;
					for(int i5=1;i5<=3;i5++){
						if(i1+i2+i3+i4+i5>=n) break;
						for(int i6=1;i6<=3;i6++){
							if(i1+i2+i3+i4+i5+i6>=n) break;
							for(int i7=1;i7<=3;i7++){
								if(i1+i2+i3+i4+i5+i6+i7>=n) break;
								for(int i8=1;i8<=3;i8++){
									if(i1+i2+i3+i4+i5+i6+i7+i8>=n) break;
									for(int i9=1;i9<=3;i9++){
										int x = n-i1-i2-i3-i4-i5-i6-i7-i8-i9;
										if(x>=1 && x<=3){
											//使用sprintf输出到字符串中 
											sprintf(s[tot++],"%d %d %d %d %d %d %d %d %d %d\0",i1,i2,i3,i4,i5,i6,i7,i8,i9,x);
										}
									}
								}
							}
						}
					}
				}
			}
		}
	}
	printf("%d\n",tot);
	for(int i=0;i<tot;i++){
		printf("%s\n",s[i]);
	}
	return 0;
}

*使用宏构造语句

观察程序排版情况就可以看出,程序嵌套层次太多,不方便阅读。这里介绍一种特殊的宏构造语句来简化程序,先来看程序代码:

#include<cstdio>
#define rep(i,a,b) for(int i=a;i<=b;i++) 
//存储多个字符串的二维char数组 
char s[60000][25];
int main(){
	int n,tot=0;
	scanf("%d",&n);
	//特判 
	if(n>30 || n<10){
		printf("0\n");	
		return 0;
	}
	//暴力枚举,记录方案
	rep(i1,1,3) rep(i2,1,3) rep(i3,1,3) rep(i4,1,3) rep(i5,1,3)
		rep(i6,1,3) rep(i7,1,3) rep(i8,1,3) rep(i9,1,3) rep(i10,1,3)
			if(i1+i2+i3+i4+i5+i6+i7+i8+i9+i10==n){
				//使用sprintf输出到字符串中 
				sprintf(s[tot++],"%d %d %d %d %d %d %d %d %d %d\0",i1,i2,i3,i4,i5,i6,i7,i8,i9,i10);
			}
	printf("%d\n",tot);
	for(int i=0;i<tot;i++){
		printf("%s\n",s[i]);
	}
	return 0;
}

程序开始处定义了宏构造语句define rep(i,a,b) for(int i=a;i<=b;i++),在程序里使用rep(i1,1,3),程序编译时会被编译器按照宏定义语句自动替换成for(int i1=1;i1<=3;i1++),就像字符串替换一样,其它使用rep的语句类似。

需要注意的是,在编译处理时,编译器只是简单的将宏调用语句按宏定义规则执行字符串替换。例如定义了一个宏define muilt(a,b) a*b,使用语句muilt(c+d,e),会被替换成c+d*e,并不是想要的(c+d)*e。解决方法就是在宏定义时勤加括号,例如这里使用define muilt(a,b) (a)*(b),就能有效避免宏替换时带来的运算优先级问题。