刘汝佳:《算法竞赛入门经典》 三步: 基本的数据结构+算法知识; 数论等数学基本知识; 锻炼联想建模能力、知识与实际相结合,解决实际问题! 第一章:程序设计入门 1.a/b 当a、b为整数时,结果向中间位置靠拢;例如: -8/5 结果为 -1 ; 2.浮点数使用%.3lf 输出; 另外: 整数-浮点数=浮点数 整数/浮点数=浮点数; 3.在竞赛中尽量使用const int 的形式来定义常量( 定义同时初始化); 在eclipse中定义目前最大为10位; #define 好像最大也是10位的样子; 第二章:循环结构程序设计 1.找不出错误的时候,也可以使用输出中间结果的方法 ,来进行差错; 2.学会使用伪代码来初步编写程序! 可以适当使用汉字等, 之后将汉字部分改写成函数,进行调用即可。 例如: for(a=1;a<=9;a++) for(b=0;b<=9;b++) if(aabb是完全平方数) printf("%d",aabb); 之后将if()括号中内容,改写成函数调用。 这个有一点迭代式开发的意思,即每次只添加程序中的一小部分内容,保证正确性。 3.开平方可能会出现浮点误差, 有时可能会使用枚举平方根的操作,来避免开平方操作; 4.一个运算表达式的取余运算, 可以将取余操作设置到每一步计算的后面,结果不变; 5.scanf()返回值为成功输入的变量个数; 第三章:数组和字符串 1.memset(f,0,siziof(f)); 将数组f初始化为0; 2.最长回文子串: 这里解决了3个问题:首先是输入字符串中含空格,这里使用了fgets()函数;其次预处理得到不含标点且全为 大写的字符串;最后是:当判断当前子串为最大回文子串时,其在原来字符串中的位置不确定,这里新开一个数组 保存其原先的位置下坐标! char f[N]; fgets(f,sizeof(f),stdin); 判断方法的改进: 枚举回文串的中间位置!然后不断向外扩展。 3.printf("%d %o %x\n",a,b,c); // 分别对应输出十进制,八进制,十六进制; 4.strchr 函数的功能:在一个字符串中查找一个字符。--------------(这个的应用在字符串题目中应该比较广泛了) 5.如果按照自己的方式处理字符串的时候,一定要保证字符串以 '\0' 结尾! 第四章:函数和递归 1.函数调用过程中,碰到return语句时,会直接退出该函数; 2.在编写函数的时候,应尽量避免函数缺陷;如若不然,应在显著位置上标明函数的缺陷,以免误用。 3.在编写程序时,尽量少使用全局变量,比较危险。 4.设计递归函数的重点:在于给下级安排工作! -----------------------------------算法篇----------------------------------- 第五章:基础题目选解 1.使用常量简化代码。 一般来说是常量字符串,先保存起来一些需要用到的东西。 2.typedef 定义结构体。 高精度运算。(75页) 3.qsort 给整数和字符串排序。 4.预处理的强大作用: 例题:字母重排问题。(78页) 提前的预处理,将每个单词直接重排字母顺序之后再保存下来会为后面省略很多麻烦。 5.因子和阶乘 6.三角形有向面积计算及其应用: 判断一个点是否在一个三角形ABC内: 计算Sabc == Soab + Sobc + Soca ? 是否成立。 这里的面积为有向面积。 计算方式为:(85页) |X0 Y0 1| 2S= |X1 Y1 1| |X2 Y2 1| 行列式的计算方式。三点为逆时针排列,结果为正。否则为负。 7.一个平面上,计算结果与 顶点、边、面的个数有关时,可能会用到欧拉公式: V-E+F=2 分别对应:顶点、边、面。 //------------- 88页有一些推荐题目,等回头挨着做了。 第六章:数据结构基础 1.关于排队队列的问题,一般可以设置两个标志变量。 front与rear,分别指向头和尾。 然后使用while(front<rear)进行判空。 2.使用栈时,一般从下标1开始进行计数; 需要一个数组和一个栈顶指针。 3.当移动次数较多时,一般使用链式结构。 4.二叉树的结构:小球从顶部从上至下滑落,除了直接模拟求小球最终落点之外,还可以按照小球的奇偶行来进行判断。 5.图:网状关系,递归求解的比较多。 包括深搜dfs、广搜bfs。 另外,如果图过大,递归dfs或者bfs容易造成栈溢出的危险,应想其它方法解决。 6.求解迷宫及其路径:宽度优先便利较为适用! 每次路径长度都是固定+1,其父节点即为当前刚从队列中出来的节点。 7.dfs/bfs 都需要做的一点就是,都需要避免节点的重复访问。 第七章:暴力求解法:枚举、基于枚举的回溯、深搜、广搜、剪枝操作等。 1.枚举排列: (1)生成1-n的排列: 递归实现: 函数参数:数组的元素个数、当前已经确定的数组A,当前需要填写的位置cur。 选择下一个要填写的元素时:是按照从小到大枚举,查找第一个不在A中的元素。 尤其注意cur一定要设为局部变量!不然需要还原数据。 (2)生成可重集的排列: 之前是设置一个标志变量ok,现在改为设置两个c1、c2,当c1<c2即可继续递归调用; 另外,为了防止重复,在枚举for循环下,加上一个判断, if(!i || p[i]!=p[i-1]) ,即可防止重复! (3)下一个排列: 这里是调用的一个C++的库函数来实现的! 先得到当前序列,之后调用 next_permutation(p,p+n); 之后输出p即可。(该库函数是直接将数组p进行了改变!) 注意:该函数同样适用于可重集! 2.子集生成:(与排列生成算法很像) (1)增量构造法: 先排序,每次增加一个元素。 函数参数与排列生成相同。 (2)位向量法: 思路:每个元素是否选择,当全部元素都判断完毕是否选择之后,输出当前集合A(if(cur==n) 来进行判断)。 参数相同。 但因为传的数组A,是地址,改变之后类似于改变全局变量,这里需要进行一个相当于还原的操作!! 代码: B[cur]=1; //选择第cur个元素! Print(n,B,cur+1); B[cur]=0; //还原数据!不选择第cur个元素! Print(n,B,cur+1); 3.回溯法:在枚举的基础上(设置局部变量)进行的! ----------是否属于深搜里面的剪枝范围? 在构造解答树时,发现某支路不通(某分支),则停止此路的枚举,从而达到减少枚举量的效果!! 注意:如果回溯法中使用了辅助的全局变量! 那么在函数的多个出口处(即调用函数结束的地方),一定要恢复被修改的值! 回溯的定义:当发生某种情况不合适时,递归函数将不再递归调用它本身,而是返回上一层调用,我们称这种现象为回溯。 (只是断了某条路,减少枚举量的操作,其返回上一层的含义为:函数中有循环,循环中有调用函数操作!在不断地调用函数。) (1)八皇后问题: (2)素数环: 法一:利用next_permutation()函数枚举全部排列。----枚举量特别大,时间效率很低; 法二:回溯法:在递归时多加了一个判断,减少枚举量! 另外设置了一个vis标志数组,由于是全局变量,故:存在还原, 清除标志的操作! (3)困难的串: 基于深搜的枚举算法。 与八皇后、素数环问题不同的是:前两者存在回溯的情况,即深搜时,存在某种情况不满足题目要求,使得这一支路全都不 满足,不必再向下继续深搜。 而困难的串,是一直深搜下去,不存在不满足题目要求的情况,直到找到第i个结果,输出即可! (4)带宽:(剪枝) 深搜下的剪枝,枚举时记录下当前带宽的min值。 当后面深搜枚举排列的时候,某一步出现了大于min值的带宽值,则不再继续向下深搜。减少枚举量! 4.隐式图搜索:(上面主要讲深搜+回溯剪枝; 下面基本都是广搜) (1)隐式图遍历: 最优程序、埃及分数: 一个是求最短的程序(同时要理解深度是无限的!一条路不可能枚举完),一个是求最小数量的数字加法和等于n。 两者都是枚举深度,之后运用队列,在深度不断加深的情况下,看每一层是否存在合适解。 但这里的埃及分数注意下:其每一层的大小也是无限的。这里枚举深度之后,运用迭代加深搜索: 在枚举深度之后,在每次的情况下再进行深搜,运用深搜进行剪枝。。 (2)倒水问题: 每次倒水都有不同的步骤,不同的情况可以走。 这里找最少的操作次数,采用深度优先遍历。 内部使用队列,每一步的循环枚举不同的倒水的情况,之后放入队列,去广搜!(同时要保证搜索结点情况不重复!) 注意:树中的广搜不会让节点重复,但是对于图中:是可能重复的,所有要加上一步判重的操作! (3)八数码问题: 广搜! 从开始状态,枚举每一次可能出现的情况,加入队列,不断广搜(这题枚举量有限,广搜有限),搜到队列为空还没找到结果时! 则证明无结果,另函数返回0。 其中的判重操作,133页提供了3个方法:一个是排列变整数;一个是哈希的链式表达;一个是用STL中的集合。 总结第七章: 这一章主要讲了基于枚举情况下的回溯、深搜+剪枝、广搜等一些情况! 在做题目时,要分清楚题目要求情况,是运用哪种方法来进行解答,但也要理解其都是基于枚举的操作之上的。 但好的枚举方法会将不必要的枚举操作省去,这也往往是题目中的考点,需要仔细分析题目,找出约束条件。 有时深搜的情况无限时,可以适当限定深搜深度d。 这方面还不是太熟,先理解概念,之后多加练习吧。 第八章:高效算法设计(第七章讲的是枚举以及基于枚举的各种优化; 这节讲的是如何将枚举利用其它方法,例如二分方法,进一步地 降低一个枚举数量的等级。) 分治、归并 ,快排与二分有着异曲同工之妙,思想上有很多类似的地方! 很多题目可以利用二分枚举来解答,大大减少枚举量。 1.最大连续和: 全部枚举:O(n^3); 利用S[j]-S[i-1],前缀和之差:O(n^2); 利用分治: 划分子问题-递归解决子问题-合并:O(n*log2n)。 (其中分治法的合并部分需要着重理解练习一下, 书上141页。) 另外,划分区间时: 令: m=x+(y-x)/2; 可以让m总是靠近区间的起点。(即向左取整。) m=(x+y)/2是向0取整。 2.n最大为10000时,最高使用n^2的时间复杂度; n最大为10^6时,最大使用n*log2n的时间复杂度。 排序与检索: 3.归并排序:附加空间n,(函数调用的参数中又多加了一个int *T) 应用:这里利用归并排序做了一个求解逆序对数的题目!巧妙地运用了归并排序的排序过程! 只需要将归并排序中的else 后面部分多加一句: cnt+=m-p; 即可! 4.快速排序: 应用:求第k小的数:这里的求解第k小的数字,我们在快排的基础上,不是将全部数字排列,只排列一部分即可: 即在排列过程中,根据左边元素个数q-p+1和k的大小关系比较,进而只在左边或者右边继续递归,剩下的一部分不需要管。 在期望意义下,时间复杂度为O(n)。 5.二分查找: 应用一:在排序的基础之上,进行二分查找,比较方便快捷。 应用二:但是!有时候二分查找也会应用在一些抽象的场合,没有数组A,没有要查找的v,但是二分查找的思想仍然适用! 例如:最大值最小化问题:将一个数组n划分为m个连续的子序列,使每段序列之和的最大值尽量小! 这里利用二分的方法:先找到一个下界x,一个上届y;之后在x与y之间二分枚举这个序列和! 递归与分治: 6.棋盘覆盖、循环日程表、巨人与鬼等! 7.非线性方程求根!(这个求解x的过程有一个约束条件,即保留一定量的小数位!) 本题思路:根据题目得知解x的一个范围! 之后二分枚举解,看是否满足题目要求。 这里因为x为实数,所以判断多项式是否相等,是用的 while(y-x>1e-5){} 来进行的判断。 贪心法:(共7个经典例子) 这里主要举3个例子: 8.选择不相交区间: 9.区间选点问题: 上述两个问题都是在基于将每个区间按照bi排序之后,贪心即可。 10.区间覆盖问题:选择尽量少的区间覆盖一条指定的线段[a,b] 将每个区间按照ai进行排序,之后每次选择以a开头,最长的区间,重复操作即可。 -----------------------------------竞赛篇----------------------------------- 第九章:动态规划初步:要掌握理解动态规划的基本思想、经典实例+锻炼利用例如二元关系建模的能力。 1.数字三角形中的动态规划: 法一:递归枚举所有情况,使用回溯法。 超高的枚举量! 法二:递推 法三:记忆话递归枚举,由于记忆话的存在,不会重复计算节点值,时间复杂度与递推类似。 2.DAG上的动态规划:(有向无环图) (1)嵌套矩形:求解从结点i出发的最长路径,以及打印出字典序最小的方案!(起点和终点都不固定。) 状态转移方程: d[i]=max(d[j]+1); (i,j)之间存在路径 (d[i]的含义:表示从结点i出发的最长路长度!) 递归代码:(记忆化搜索) int dp(int i){ int &ans=d[i]; if(ans>0) return ans;//这两步进行了记忆化的操作,免除很多重复操作! ans=1; for(int j=1;j<=n;j++) if(G[i][j]) ans >?= dp(j)+1;//这里进行了递归调用 return ans; } 求解字典序最小的最长路径: 字典序最小的首要因素还是长度,次要因素是并列时才进行的比较。 在求解出d[]的值后,选择其中最大的d[i]所对应的i,如果有多个,选择其中最小的一个i进行下面递归调用。 void Print(int i){ printf("%d",i); //这个起点已经确定,可直接输出。与硬币问题不太相同,硬币问题需要在下面if中判断之后再输出! for(int j=1;j<=n;j++){ if(G[i][j] && d[i]==d[j]+1){ //判断是否存在这个边! Printf(j); break; } } } (2)硬币问题:固定终点的最长路和最短路! 最长路求解: d[i]的含义:从结点i到结点0的最长路径长度! 调用dp(S); //是不是有一些边界的初值要赋予?作为递归的边界? int dp(int S){ if(vis[S]) return d[S]; //因为这个题目中路径长度本身是可以为0的,故不能用d[]==0来判断这个d[]值是否判断过; //标志数组初始化: memset(vis,0,sizeof(vis)); vis[S]=1;//标记访问过! int &ans=d[S]; ans= -1<<30; for(int j=1;j<=n;j++) if(S>=V[i]) ans >?= dp(S-V[i])+1;//递归调用! v[i]表示当前路径长度! return ans; } 递推的方法: min[0]=max[0]=0; for(int i=1;i<=S;i++){min[i]=INF; max[i]=-INF;} for(int i=1;i<=s;i++) for(int j=1;j<=n;j++) if(i>V[j]){ min[i] <?= min[i-V[j]]+1; max[i] >?= max[i-V[j]]+1; } printf("%d %d\n",max[S],min[S]); 输出路径: void Print(int *d,int S){ for(int i=1;i<=n;i++) //输出字典序最小的,从i=1开始进行循环! if(S>=v[i] && d[S]==d[S-v[i]]+1){ //判断当前路径是否在之前被选择的 核心条件! printf("%d ",i); Print(d,S-v[i]); break;//这里需要加上跳出循环! } } 3.0-1背包问题: 多阶段决策问题: 0-1背包 状态转移方程: f[i][j]=max(f[i-1][j],f[i-1][j-V[i]]+W[i]); (f[i][j]表示把前i个物品装到容量为j的背包中的最大总重量! 状态转移方程即为: 第i个物品选与不选的问题!) for(int i=1;i<=n;i++) for(int j=0;j<=C;j++){ f[i][j]=(i==1 ? 0 : f[i-1][j]); if(j>V[i]) f[i][j] >?= f[i-1][j-V[i]]+W[i]; } //这里也可以不保存边,边读入边进行计算;(不必保存V、W的值) 4.递归结构中的动态规划: (1)表达式上的动态规划:(抽象状态 + 设计转移) 最优矩阵链乘:一系列矩阵相乘,设计一种方法,使得总的运算量最小。 思路:肯定存在"最后一次乘法",将整个表达式分为两部分:P和Q,只需让P和Q按照最优方案计算即可!(最优子结构) (枚举最后一次乘法的位置!) 状态转移方程: f[i][j]=max(f[i][k]+f[k][j]+Pi-1PkPj); k从i-j枚举! (f[i][j]表示: 从Pi到Pj需要多少次乘法。) 边界为: f[i][i]=0; (2)凸多边形上的动态规划: 状态转移方程: f[i][j]=max(f[i][k]+f[k][j]+W(i,j,k)); f[i][j]表示从顶点i到顶点j所构成的子多边形的最大三角部分权和。 (注意:因为是圆环式的结构,j可能比i大,需要枚举的k为:i+1,i+2...n,1,2...j-1) ---与最优矩阵链乘的枚举方面的区别。 (3)树上的动态规划: 树的最大独立集:选出尽量多的点,使得任何两个结点均不相邻。 (这个题目肯定需要全部结点的遍历,这里记录下了每个结点的父亲节点。) 过程分为:第i个结点选与不选。 状态转移方程 : d[i]=max(1+求和(d[j]), 求和(d[j])); 第一个j为i的所有儿子,第二个j为i的所有的孙子。 此为填表法! 由于这里的儿子和孙子不好进行枚举,时间复杂度过高,这里使用另外一种刷表法。 (刷表法没看太懂,不知道具体实现如何,书172页) 5.集合上的动态规划:(暂时还有困难,未总结) 小总结: 递归与非递归动态规划的区别! 递归的状态转移方程中:需要使用到的值不知道!需要不断递归到底层才能知道其结果! 而非递归的动态规划方程中,'='号后面的值都是已经计算出来的,所以不需要递归来进行计算。 (递归的动态规划中,一定要注意边界以及使用记忆化的方法,减少递归量!才能有效保证复杂度不过高!)
2014.3.11补充总结: 动态规划: 1.走路的动规;(DAG上的动规) 2.层次划分的动规---划分子问题:(这里不存在选与不选的问题!)
递归中的动态规划:看能否化简计算规模---当前与化简后规模的关系--->本题是指 f(i,j)=max(下一级的简化问题) 3.选与不选的动规; 4.集合上的动规。 总的考虑思路: 先看问题是否可以划分为子问题,之后尝试使用数组尝试说明状态,当无法叙述清楚时考虑增加维度!
第十章:数学概念与方法:(数论初步、排列组合、递推关系、离散概率) 数论初步:(数学王国的皇后)本章介绍几个最为常用的算法,以及一些实例展现出一些常用的思维方式。 (看着是个数论题,拿出几种情况试一下,一般会发现一些规律在里面。) (数论一般都是需要根据题意以及数学知识,简化题目问题之后求解!所以找规律和特点还是比较重要的。) 1.除法表达式: 因为第2个数一定是分母,其它的数都可以为分子,所以这里直接计算X1*X3*X4*.../X2是否为整数即可; 容易证明,当分母不止一个数时,X2一定在其中! 之前叙述的只有X2做分母的情况是一切生成整数情况的前提! ------这里证明是否整除利用了另外一个数论知识:辗转相除与约分素因子。 (将分母约减为1,则证明该分数可以划分为整数!) 2.无平方因子数: 与素数筛选法类似:对于不超过根号m的所有素数p,筛选掉区间内所有p^2的倍数! 补充:素数定理:179页,π(x)表示:不超过x的素数的个数。 π(x)= x/ln(x); (10^6中大约不到8W个素数;) 3.直线上的点:(与表达式有关!需要数学知识简化方程: 179页) 4.同余与模运算: (1)a,b两数对n取模: 注意a-b时,可能有负数; a*b时可能存在溢出! 解决: (a-b)%n=((a%n)-(b%n)+n)%n; a*b%n=(int)((long long)(a%n)*(b%n)%n); (2)大整数取模运算: 模板:在博客园中。 (3)模线性方程:(同余运算的问题!) 排列与组合: 5.杨辉三角与二项式定理: 与组合数相关的最重要的两个东西: 杨辉三角和二项式定理。 问题:对于给定的a,b,n,如何求(a+b)^n的所有项的系数? (1)从杨辉三角的第一行进行递推; O(n^2) (2)利用公式: C(n)(k)=((n-k+1)/k)*C(n)(k-1),从C(n)(0)=1开始,只递推第n行。 O(n) C[0]=1; for(int i=1;i<=n;i++) C[i]=C[i-1]*(n-i+1)/i; //输出数组C即可。 6.无关的元素:也是与组合、杨辉三角有关! 7.数论中的计数问题: (1)约数的个数:----注意:一般求解个数的题目,当计算到某一步的时候,都可以进一步利用公式降低时间复杂度! 求解n的正约数的个数: 先求出每个素因子的次方数,则其正约数个数即为: (a1+1)(a2+1)...(ak+1); 另外,如果给定的n过大,也可以利用前面的大数取模模板进行调用。 (2)小于n且与n互素的个数。(184页) 8.编码与解码: 9.离散概率初步:(187页) 递推关系: 10.汉诺塔: 递推公式: f[n]=f[n-1]+1+f[n-1]; 也即 f[n]=2^n-1; 数学归纳法是一种利用递归思想证明的方法:如果要讨论的对象具有某种递归的性质,可以考虑使用数学归纳法。 11.Fibonacci数列: 问题描述:楼梯有n个台阶:上楼可以一步上1个台阶,也可以一步上2个台阶,讨论一共有多少种上楼的方法? 思路:走一步时,还有n-1步要走;走两步时,还有n-2步要走。 f[n]=f[n-1]+f[n-2]; (即为fibonacci数列) 问题2描述:生兔子问题: 第n个月的兔子有两部分组成:一部分是上个月就有的老兔子,一部分是上个月出生的新兔子。 也即: f[n]=f[n-1]+f[n-2]; 问题3描述:2*n的长方形方格:铺骨牌! 问有多少种铺法。 12.Catalan数: (191页之后,到本章结束!) 第十一章:图论模型与算法 再谈树: 1.无根树转化为有根树:(任选一点作为根节点,之后广度优先遍历,确认每个点的父亲结点.) 树是一种特殊的图,这里没有采用链式结构,采用STL中的一种数据结构,可变长数组! vector vector<int> G[max]; //建立树结构 void tree(){ int u,v; scanf("%d",&n); for(int i=0;i<n-1;i++){ scanf("%d %d",&u,&v); G[u].push_back(v); G[v].push_back(u); } } //转化过程,确认每个结点的父结点: i结点的父节点为p[i]; (主程序中设置p[root]=-1;表示根节点的父结点不存在。 然后调用dfs(root,-1)即可。) void dfs(int u,int fa){ int d=G[u].size(); for(int i=0;i<d;i++){ int v=G[u][i]; if(v!=fa) dfs(v,p[v]=u); } } 2.表达式树:(197页) 3.最小生成树:有两种:Prim与Kruskal(这里只介绍Kruskal,易于编写,而且效率很高。) 第一步:给边按照从小到大的顺序排序;------这里提到一个间接排序的概念。 第二步:依次考虑每条边(u,v)是否在同一个连通分量中; ------这里用到了并查集!! 并查集:使用树来表示集合。---相关代码在最小生成树的代码中体现。 Code:
#define N 100010 int m,n;int p[N],r[N],w[N];int u[N],v[N]; int cmp(int i,int j){return w[i]<w[j];} //间接排序函数! int find(int x){return p[x]== x ? x:p[x]=find(p[x]);} //并查集的find,寻找结点x的父节点。 int Kruskal(){ int ans=0; for(int i=0;i<n;i++) p[i]=i; //初始化并查集 for(int i=0;i<m;i++) r[i]=i; //初始化边序号,便于间接排序!之后第i小的边序号保存在r[i]中 sort(r,r+m,cmp);//给边排序 for(int i=0;i<m;i++){ int e=r[i]; int x=find(u[e]);int y=find(v[e]); //找出当前边两个端点所在集合的编号! if(x!=y){ ans += w[e]; p[x]=y; //如果不在同一集合中,合并! x合并到y下,成为y的子树 } } return ans; //ans即为最终最小生成树的权值! }
//依次输入边和对应权值。—-如果村庄编号是从1-n,初始化并查集那里需要改成i<=n!!!
4.最短路问题: 一点到另一点的最短路径 (202页之后) 注意事项: 1.做多步的取余运算时,例如式子: num=num*2%10000; f[i]=(num-f[i-1])%10000; 注意这里可能会出现f[i]为负数的情况!!需要进一步判断。 2.while(scanf("%d:%d %d:%d",&a,&b,&a1,&b1)!=EOF) 是可以进行输入的; 3.在一个循环中进行输入的时候,千万不能出现break语句,一定会出错! 4.求解迷宫,一般用宽度优先,每次路径长度增加1,较为方便。 5.有一类题目给出的n或者m比较大的,这个时候如果是枚举的题目,肯定是要根据题目特点(利用公式)减少 枚举量的,不然一般都会超时。 6.有一些数学类的题目,根据题意,看透问题本质是最重要的。 7.hdu1058,又学习了一种枚举的方法! int i,in1=0,in2=0,in3=0,in4=0,M1,M2,M3,M4; 取最小值,从哪取,哪个再向上乘。 8.有的题目,是枚举一部分,在枚举之后,使用优化的方法去计算或者查找另外的一些值。 进而进行求解。 例如hdu上2428,查找正方形的题目:枚举两点,二分查找另外两点是否存在,进而判断该正方形是否存在。 9.唯一分解定理:每一个数都可以写成多个素数相乘的模式!