630 likes | 867 Views
程序设计实习. 习题课. 枚举. 枚举 : 在条件范围内对相关变量依次取值,列出所有可能的情况,逐一检查是否是问题的解 关键: 有序地枚举解空间,不漏掉情况 尽早发现不是解的情况 枚举中的优化. 搜索:复杂的、高级的枚举. 搜索 : 解空间的每个元素是一个动作的序列 F 将初态 S 0 变换为另一个状态 F(S 0 ) 如果 F(S 0 ) 是符合要求的 S*, 那么 F 是真解 , 否则是伪解 按照一定规则 , 确定在每个状态 S 下 , 分别有哪些动作可供选择 深度优先搜索,广度优先搜索 需要对遍历过的状态进行标记
E N D
程序设计实习 习题课
枚举 • 枚举:在条件范围内对相关变量依次取值,列出所有可能的情况,逐一检查是否是问题的解 • 关键: • 有序地枚举解空间,不漏掉情况 • 尽早发现不是解的情况 • 枚举中的优化
搜索:复杂的、高级的枚举 • 搜索: 解空间的每个元素是一个动作的序列F • 将初态S0变换为另一个状态F(S0) • 如果F(S0)是符合要求的S*, 那么F是真解, 否则是伪解 • 按照一定规则, 确定在每个状态S下,分别有哪些动作可供选择 • 深度优先搜索,广度优先搜索 • 需要对遍历过的状态进行标记 • 采用递归的办法, 产生每个动作序列 • 递归的出口:达到目标状态;状态已经遍历过
递归 • 递归: 从目标出发,把一个问题逐级分解成子问题 • 子问题与原问题之间是纵向的、同类的关系 • 语法形式上:在一个函数的运行过程中,调用这个函数自己 • 直接调用:在fun()中直接执行fun() • 间接调用:在fun1()中执行fun2();在fun2()中又执行fun1() • 关键: • 递推关系 • 终止条件 • 递归算法思路清晰,程序易读易写
动态规划 • 动态规划:采用自底向上的递推算法 • 从已知条件出发,将计算出的结果保存起来,直接用于后续计算。 • 避免树形递归的大量重复运算。 • 同样需要寻找递推关系
小结 • 根据题意,以及自己对方法的熟悉程度,灵活选择各种方法,综合使用 • 直接解决 • 枚举,(搜索) • 递归,转化为规模更小的同类问题 • 遇到具有多阶段过程的问题,先走一步试试,考虑如何将问题转化(如考虑第一步或最后一步) • 难的不会想简单的
poj 2766 最大子矩阵 • 题目描述 已知矩阵的大小定义为矩阵中所有元素的和。给定一个矩阵,你的任务是找到最大的非空(大小至少是1 * 1)子矩阵。 • 比如,如下4 * 4的矩阵:0 -2 -7 0 9 2 -6 2-4 1 -4 1-1 8 0 -2 其最大子矩阵是:9 2-4 1-1 8 这个子矩阵的大小是15。
poj 2766 最大子矩阵 • 输入 输入是一个N * N的矩阵。输入的第一行给出N (0 < N <= 100)。再后面的若干行中,依次(首先从左到右给出第一行的N个整数,再从左到右给出第二行的N个整数……)给出矩阵中的N2个整数,整数之间由空白字符分隔(空格或者空行)。已知矩阵中整数的范围都在[-127, 127]。 • 输出 输出最大子矩阵的大小。
poj 2766 最大子矩阵 • 样例输入 4 0 -2 -7 0 9 2 -6 2 -4 1 -4 1 -1 8 0 -2 • 样例输出 15
解题思路 • 枚举:依次计算每个子矩阵的值。 i、j分别为子矩阵的纵坐标。i=0时: 0 -2 -7 0 9 2 -6 2-4 1 -4 1-1 8 0 -2 b[N]:存放子矩阵每列的和 则问题转换为寻找b[N]的最大子序列。
参考程序 for(int i = 0; i < N; i++){//遍历子矩阵,i、j为子矩阵的 //两个纵坐标 memset(b,0,sizeof(b)); //每次均初始化b数组 for(int j = i; j < N; j++){ for(int k = 0; k < N; k++){//压缩数组,将求最大子矩阵 //转换为求b的最大子序列问题 b[k] += arr[j][k]; } int tempMax = maxLine(b, N); if(tempMax > result) result = tempMax; } } printf("%d\n",result);
参考程序 int maxLine(int a[], int n){ int m = -10000, s = 0; for (i = 0; i < n; i++){//依次计算从序列中的i个元素开始的 //最大子序列值 if (s < 0){ s = 0; } s += a[i]; if (m < s) m = s; } return m; }
poj 2950 摘花生 • 题目描述 • 鲁宾逊先生有一只宠物猴,名叫多多。这天,他们两个正沿着乡间小路散步,突然发现路边的告示牌上贴着一张小小的纸条:“欢迎免费品尝我种的花生!——熊字”。鲁宾逊先生和多多都很开心,因为花生正是他们的最爱。在告示牌背后,路边真的有一块花生田,花生植株整齐地排列成矩形网格(如图1)。有经验的多多一眼就能看出,每棵花生植株下的花生有多少。为了训练多多的算术,鲁宾逊先生说:“你先找出花生最多的植株,去采摘它的花生;然后再找出剩下的植株里花生最多的,去采摘它的花生;依此类推,不过你一定要在我限定的时间内回到路边。”
poj 2950 摘花生 • 我们假定多多在每个单位时间内,可以做下列四件事情中的一件:1) 从路边跳到最靠近路边(即第一行)的某棵花生植株;2) 从一棵植株跳到前后左右与之相邻的另一棵植株;3) 采摘一棵植株下的花生;4) 从最靠近路边(即第一行)的某棵花生植株跳回路边。 • 现在给定一块花生田的大小和花生的分布,请问在限定时间内,多多最多可以采到多少个花生?注意可能只有部分植株下面长有花生,假设这些植株下的花生个数各不相同。
poj 2950 摘花生 • 例如在图2所示的花生田里,只有位于(2, 5), (3, 7), (4, 2), (5, 4)的植株下长有花生,个数分别为13, 7, 15, 9。沿着图示的路线,多多在21个单位时间内,最多可以采到37个花生。
poj 2950 摘花生 • 输入 输入的第一行包括一个整数T,表示数据组数每组输入的第一行包括三个整数,M, N和K,用空格隔开;表示花生田的大小为M * N(1 <= M, N <= 50),多多采花生的限定时间为K(0 <= K <= 1000)个单位时间。接下来的M行,每行包括N个非负整数,也用空格隔开;第i + 1行的第j个整数Pij(0 <= Pij <= 500)表示花生田里植株(i, j)下花生的数目,0表示该植株下没有花生。 • 输出 输出包括T行,每一行只包含一个整数,即在限定时间内,多多最多可以采到花生的个数。
解题思路 • 如何存储每棵植株 • 可用结构数组 struct plant{ int x; int y; int num; }plant[2501]; • 对每棵植株按照花生多少进行排序 • 依次判断是否够时间摘下每棵植株上的花生
参考程序 #include<iostream> using namespace std; struct huasheng{ int x; int y; int num; }hs[2501]; int compare(const void* a,const void* b){ struct huasheng *w = (struct huasheng *) a; struct huasheng *v = (struct huasheng *) b; return v->num - w->num; }
int main(){ int T; scanf("%d",&T); while(T --){ int M,N,K; scanf("%d%d%d",&M,&N,&K); int num = M*N; int i = 0,j = 0,k = 0; while(num --){ scanf("%d",&hs[k].num); hs[k].x = i; hs[k].y = j / N; i = (i + 1) % N; j ++; k ++; } qsort(hs,M*N,sizeof(huasheng),compare);
int sum = 0,time = 0, l = 0; while(time < K){ if(hs[l].num == 0||l >= M*N) break; if(l == 0) time = time + 2*(hs[l].y + 1) + 1; else time = time + abs(hs[l].x - hs[l - 1].x) + abs(hs[l].y - hs[l - 1].y) + 1 + hs[l].y + 1; if(time > K) break; else{ sum += hs[l].num; time -= (hs[l].y + 1); } l ++; } printf("%d\n",sum); } }
poj 2788 二叉树 • 由正整数1,2,3……组成了一颗二叉树。我们已知这个二叉树的最后一个结点是n。现在的问题是,结点m所在的子树中一共包括多少个结点。 • 比如,n = 12,m = 3那么上图中的结点13,14,15以及后面的结点都是不存在的,结点m所在子树中包括的结点有3,6,7,12,因此结点m的所在子树中共有4个结点。
poj 2788 二叉树 • 输入 输入数据包括多行,每行给出一组测试数据,包括两个整数m,n (1 <= m <= n <= 1000000000)。最后一组测试数据中包括两个0,表示输入的结束,这组数据不用处理。 • 输出 对于每一组测试数据,输出一行,该行包含一个整数,给出结点m所在子树中包括的结点的数目。
解题思路 • 此题有多种做法 • 一种做法是:从m点出发,逐次计算其子树第k层的最小节点minNode和最大节点maxNode,判断节点n是否在m子树的第k层中,如果n>maxNode,则继续计算节点第k+1层,否则分n<minNode和n>=minNode两种情况,计算m子树总结点数。
int main(){ int m=0,n=0; while(scanf("%d %d",&m,&n) != EOF){ if (m == 0 && n == 0) break; int minNode=m, maxNode=m, num=1; while(1){ minNode = minNode * 2; maxNode = maxNode * 2 + 1; if (n <= maxNode) { if (minNode <= n) num += (n - minNode) + 1; break; } num += maxNode - minNode +1; } printf("%d\n",num); } return 0;}
int main(){ int m,n,x,i,ans; cin >> m >> n; while (1){ if (m==0 && n==0) break; x = m; i = 0; while (x <= n){ x = (x<<1) + 1; i++; } if (n < (m<<i)) ans = (1<<i) - 1; else ans=(1<<i)+(n - (m<<i)); cout << ans << endl; cin >> m>> n; } return 0; }
poj 1095 Trees Made to Order • 题目描述 We can number binary trees using the following scheme: • The empty tree is numbered 0. • The single-node tree is numbered 1. • All binary trees having m nodes have numbers less than all those having m+1 nodes. • Any binary tree having m nodes with left and right subtrees L and R is numbered n such that all trees having m nodes numbered > n have eitherLeft subtrees numbered higher than L, orA left subtree = L and a right subtree numbered higher than R.
poj 1095 Trees Made to Order • The first 10 binary trees and tree number 20 in this sequence are shown below: • Your job for this problem is to output a binary tree when given its order number.
poj 1095 Trees Made to Order • 输入 Input consists of multiple problem instances. Each instance consists of a single integer n, where 1 <= n <= 500,000,000. A value of n = 0 terminates input. (Note that this means you will never have to output the empty tree.) • 输出 For each problem instance, you should output one line containing the tree corresponding to the order number for that instance. To print out the tree, use the following scheme:A tree with no children should be output as X.A tree with left and right subtrees L and R should be output as (L')X(R'), where L' and R' are the representations of L and R.If L is empty, just output X(R').If R is empty, just output (L')X.
poj 1095 Trees Made to Order • 样例输入 1 20 31117532 0 • 样例输出 X ((X)X(X))X (X(X(((X(X))X(X))X(X))))X(((X((X)X((X)X)))X)X)
解题思路 • tNum[i]:表示有i个node的不同树的个数; • tNum[0]=1; tNum[1]=1; tNum[2]=2; • tLast[j]:表示最后一棵有j个node的树的编号; • tLast[0]=0; tLast[1]=1; tLast[2]=3; • 可以得到递推关系: tNum[i]=∑tNum[k]*tNum[i-1-k],(k = 0,1,…,i-1) tLast[j] = tLast[j-1]+ tNum[j]
#include<iostream> using namespace std; int tNum[20]; int tLast[20]; void print(int n);//输出编号为n的子树 int main(){ int n; tNum[0] = 1; tNum[1] = 1; tLast[0] = 0; tLast[1] = 1; for(int i = 2; i < 20; i++){ tNum[i] = 0; for(int j = 0; j < i; j++) { tNum[i] += tNum[j] * tNum[i-j-1]; } tLast[i] = tLast[i-1] + tNum[i]; } while(scanf("%d",&n) && n){ print(n); printf("\n"); } return 0; }
void print(int n) {//输出编号为n的树 int i,j,t; for(j=1;;j++){ //找到n对应的树有几个节点:j if( n <= tLast[j] ) break; } n = n - tFirst[j-1]; for(i = 0; i < j; i++){ //找到其左子树有几个节点:i t = tNum[i]*tNum[j-1-i]; if(n > t) n = n - t; else break; } if(i != 0){ printf("("); print(tLast[i-1] + 1 + (n-1)/tNum[j-1-i]); printf(")"); } printf("X"); if(i != j-1){ printf("("); print(tLast[j-2-i] + 1 + (n-1)%tNum[j-1-i]); printf(")"); } }
poj 2804:字典 • 词条(一行):一个英文单词、一个外语单词 • 100000 个词条 • 给外语单词,翻译出英文 • 关键点:查找的效率 • qsort,bsearch • 使用结构表示词条 • 外语单词 • 英文单词 • 搜索词条:外语单词 • 注意: qsort排序的元素类型与bsearch查找的元素类型要完全一致
poj 2797:最短前缀 • 前缀:单词前若干个字母,不是其它单词的前缀 • 2~1000行 • 可以是整个单词 • 题目关键点 • 把有相同前缀的单词放到一起: qsort • 确定它们的各自前缀 • 不是前面单词的前缀 • 不是后面单词的前缀 • 按照输入的顺序输出: qsort • 注意:两次排序,使用结构体 • 单词 • 前缀 • 输入顺序
poj 2815:城堡问题 • 与黑瓷砖上行走问题类似:枚举+递归 • 枚举:站在任何一点,开始游走 • 递归:从当前点往其他地方走,不撞南墙不回头 f(x,y)=1+f(x-1,y)+f(x+1,y)+f(x,y-1)+f(x,y+1)
poj 2790 迷宫 • 迷宫可以看成是由n * n的格点组成,每个格点只有2种状态,.和#,前者表示可以通行后者表示不能通行。 • 当Extense处在某个格点时,他只能移动到东南西北(或者说上下左右)四个方向之一的相邻格点上。 • Extense想要从点A走到点B,问在不走出迷宫的情况下能不能办到。 • 与黑瓷砖上行走问题类似 • 递归:从A点出发,向四周走; • 走过的点要进行标记。 bool f(int x, inty); f(Ax,Ay)=f(Ax-1,Ay)||f(Ax+1,Ay)||f(Ax,Ay-1)||f(Ax,Ay+1)
poj 2775 代码档案 • 样例输入 file1 file2 dir3 dir2 file1 file2 ] ] file4 dir1 ] file3 * # • 样例输出 DATA SET 1: ROOT | dir3 | | dir2 | | file1 | | file2 | dir1 file1 file2 file3 file4 输出要求:先显示目录中的子目录,然后再显示文件。 文件要求按照名字的字母表的顺序显示,目录只按照目录出现的先后显示。
递归 • 遇到文件则保存; • 遇到目录则直接输出,递归调用; • 目录结束时,对所有文件排序,按顺序输出。
#include<iostream> using namespace std; void getdir(char *dirn, int n); int mycmp(const void *elem1, const void *elem2){//比较函数,文件名排序 return strcmp((char *)elem1, (char *)elem2); } int main(){ int m = 1; //m表示第几组数据 char c; while(m){ c=cin.peek(); if(c == '#') break; //先判断是否为读入结束 cout<<"DATA SET "<<m<<":"<<endl; getdir("ROOT", 0); m++; cout<<endl; cin.get(); } return 0; }
void getdir(char *dirn, int n){ //输入并输出子目录,n为子目录的深度 char fname[100][100]; int k = 0, i, j; for(i = 0; i < n; i++) cout<<"| "; //先输出子目录的名字 cout<<dirn<<endl; char str[100]; while(cin>>str) { if(str[0] == ‘f’)//表示输入为一个文件 strcpy(fname[k++],str); if(str[0] == ‘d’) //表示输入的是一个新的目录,则输出目录 getdir(str, n+1); if(str[0] == ‘]’ || str[0] == ‘*’){ //表示此目录输入结束,输出 qsort(fname,k,sizeof(fname[0]),mycmp); for(i = 0; i < k; i++){ for(j = 0; j < n; j++) cout<<"| "; cout<<fname[i]<<endl; } break; } } return ; }
poj 2787:算24 • 枚举:运算树,多种枚举方法 • 对树的结构、节点上的运算符、及每种树的排列进行枚举 • f(1,4):左子树A和右子树B • A:[1,3],B:[4,4] • A:[1,2],B:[3,4] • 按实际做运算的先后顺序枚举 1+(2*3-4) + - 1 * 4 2 3
poj 2787:算24 • 2个数可以做6种运算: A+B、A-B、B-A 、A*B、A/B 、 B/A • 递归函数Calc(int Numbers[],n):表示对数组中的n个数进行运算,返回为1或0,1表示是可以得到24,0表示不能得到24。 • 对于Calc(int Numbers[],n),每次从数组中任取出两个数进行运算,将结果与另外n-2个数一起组成大小为n-1的数组,则问题转化成计算Calc(int Numbers[],n-1)。
#include <iostream> using namespace std; double Numbers[5]; int main(){ int a[4]; while(true) { int nZeroNum = 0; for( int i = 0;i < 4;i ++ ) {//读入数组 cin >> Numbers[i]; if( Numbers[i] == 0 ) nZeroNum ++; } if( nZeroNum == 4 )//判断是否结束 break; if( Calc(4, Numbers)) cout << "YES" <<endl; else cout << "NO" << endl; } return 0; }
bool Calc( double * pNumbers, int n ){ int i,j, k; double aNumber[4]; if( n == 1 ) { if( pNumbers[0] > 23.99999 && pNumbers[0] < 24.00001 ) return true; else return false; } for( i = 0;i < n - 1;i ++ ) { for( j = i + 1; j < n; j ++ ) {//从pNumbers中任取两个数 int m = 0; for( k = 0; k < n; k ++ ) {//将其全n-2个数放入新数组 if( k != i && k != j ) aNumber[m++] = pNumbers[k]; } aNumber[m] = pNumbers[i] + pNumbers[j];//对选取的两个数运算 if( Calc( aNumber, n -1 )) //并放入新数组 return true; aNumber[m] = pNumbers[i] * pNumbers[j]; if( Calc( aNumber, n -1 )) return true;
aNumber[m] = pNumbers[i] - pNumbers[j]; if( Calc( aNumber, n -1 )) return true; aNumber[m] = pNumbers[j] - pNumbers[i]; if( Calc( aNumber, n -1 )) return true; if( pNumbers[i] != 0) { aNumber[m] = pNumbers[j] / pNumbers[i]; if( Calc( aNumber, n -1 )) return true; } if( pNumbers[j] != 0) { aNumber[m] = pNumbers[i] / pNumbers[j]; if( Calc( aNumber, n -1 )) return true; } }//j循环结束 }//i循环结束 return false; }
poj 1088:滑雪 • 动态规划 • 按高度排序:qsort • 从高度最低的节点开始算起 数组Mlen[MaxN][MaxN]记录每个节点的最大可滑长度; Mlen[x][y]:(x,y)周围四个点中高度比它低的点的最大可滑长度+1。 Mlen[x][y] = 1 + max{ Mlen[x-1][y], Mlen[x+1][y], Mlen[x][y-1], Mlen[x][y+1]} • 节点的定义 struct Point{ int x; 注意:还需要定义h[MaxN][MaxN]; int y; int h; }pos[10010];
#include<stdio.h> #include<stdlib.h> #include<memory.h> const int dx[] = {-1, 0, 0, 1}; const int dy[] = {0, -1, 1, 0}; int h[110][110] = {0}; int mLen[110][110] = {0}; struct Point{ int x; int y; int h; }pos[10010]; int mycmp(const void *elem1,const void *elem2){ Point* p1,*p2; p1=(Point*) elem1; p2=(Point*) elem2; return p1->h-p2->h; }
int main(){ int row, col; scanf("%d%d", &row, &col); int k = 0; for (int i = 1; i <= row; i++) for (int j = 1; j <= col; j++){ scanf("%d", &h[i][j]); pos[k].x = i; pos[k].y = j; pos[k].h =h[i][j]; mLen[i][j] = 1; k++; } qsort(pos, row*col, sizeof(pos[0]), mycmp);
for (int i = 0; i < k; i++){//递推计算mLen[x][y] int x = pos[i].x; int y = pos[i].y; int mTmp = mLen[x][y]; int tmp; for (int j = 0; j < 4; j++){ if (h[x + dx[j]][y + dy[j]] < h[x][y]){ tmp=mLen[x + dx[j]][y + dy[j]] + 1; if (tmp > mTmp ) mTmp= tmp; } } mLen[x][y] = mTmp; } int result = 0;//最大滑行长度 for (int i = 1; i <= row; i++) for (int j = 1; j <= col; j++) if (mLen[i][j] > result) result = mLen[i][j]; printf("%d\n", result); return 0; }
poj 1088:滑雪 • 递归(备忘录法) • 函数MaxLen(x, y)每个点的最大可滑长度 MaxLen(x, y):(x,y)周围四个点中高度比它低的点的最大可滑长度+1。 MaxLen(x, y)= 1 + max{MaxLen(x-1, y), MaxLen(x+1, y), MaxLen(x, y-1), MaxLen(x, y+1)} • 定义数组来记录已计算过的节点的最大可滑长度