保研上机算法

本文是王道考研机试指南的读书笔记

要审题!!

一般要求复杂度在10000000内,即百万级别。2^23=8388608,10^7=10000000

基础

long long 64位,可存到9.2*10^18
int 32 位 ,可存到2.14*10^9
记得图的结点是否包含0!不包含要每个序号都减1或从1开始!
int cname[256]={0};//全赋0
int cname[256]={1};//第一个值为1,其他为0

while(scanf("%d%d", &n1, &n2)!=EOF)

while(cin>>str1>>str2)

输出记得换行

1. 查找:STL标准库 sort, 重载<

bool operator < (const pro &a) const{

    return t < a.t;

2. 日期:计算和0年0月0日的差距来计算例如两个日期差,输出周几等

注意闰年判断,提前输入每月的天数,遇到根据英文判断的要准备各个月份/星期英文名,可以提前计算以空间换时间

3. Hash:以空间换时间,直接存储值用下标查找

4. 排版:对于有较强规律的图形,可以直接以从上至下、从左至右应用规律;对于规律性不强的图形,可以先排版再输出(利用缓存数组,使用规律在数组内组合好图形),注意边界数据处理

scanf ("%d %c %c",&n,&a,&b) == 3        %c中间空格过滤输入空格

5. 查找:根据复杂度选择算法,遍历O(mn),二分O(mLogn)(要求事先排序)

6. 贪心:找贪心规律,先排序再求解

背包:按单位价值降序排序;

活动安排:按结束时间升序排序;

数据结构

1. 中缀表达式求值

    1.设立两个堆栈,一个用来保存运算符,另一个用来保存数字。

    2.在表达式首尾添加标记运算符,该运算符运算优先级最低。

    3.从左至右依次遍历字符串,若遍历到运算符,则将其与运算符栈栈顶元素进行比较,若运算符栈栈顶运算符优先级小于该运算符或者此时运算符栈为空,则将该运算符压入堆栈。遍历字符串中下一个元素。

    4.若运算符栈栈顶运算符优先级大于该运算符,则弹出该栈顶运算符,再从数字栈中依次弹出两个栈顶数字,完成弹出的运算符对应的运算得到结果后,再将该结果压入数字栈,重复比较此时栈顶运算符与当前遍历到的运算符优先级,视其优先级大小重复步骤3或步骤4。

    5.若遍历到表达式中的数字,则直接压入数字栈。

    6.若运算符堆栈中仅存有两个运算符且栈顶元素为我们人为添加的标记运算符,那么表达式运算结束,此时数字堆栈中唯一的数字即为表达式的值。

2. 哈夫曼树

priority_queue<Type, Container, Functional>

Type为数据类型, Container为保存数据的容器,Functional为元素比较方式。

如果不写后两个参数,那么容器默认用的是vector,比较方式默认用operator<,也就是优先队列是大顶堆,队头元素最大。

叶子结点的权值乘路径和=各非叶子结点之和

3. 二叉树

    1. 根据前序遍历和中序遍历推后序遍历:前序遍历内容=某字符F+F的左子树+F的右子树;中序遍历内容=F的左子树+某字符F+F的右子树。即:前序遍历中的某字符F在中序遍历串中的左侧所有字符均在F的左子树,右侧均在右子树。按照此规律遍历即可,不需要建树直接输出。

string fro, mid;
void solve(string str, char c)
{
    string left, right;
    if(str.size()>1 && c!=str[0])
    {
        left = str.substr(0, str.find(c));
        solve(left, fro[fro.find(c)+1]);
    }
    if(str.size()>1 && c!=str[str.size()-1])
    {
        right = str.substr(str.find(c)+1, str.size()-str.find(c)-1);
        solve(right, fro[fro.find(c)+left.size()+1]);
    }
    printf("%c", c);
}

    2. 二叉排序树(二叉搜索树)插入算法:①若当前树为空,则x为其根结点。②若当前结点大于x,则x插入其左子树;若当前结点小于x,则x插入其右子树;若当前结点等于x,则根据具体情况选择插入左右子树或者直接忽略。

    3. 判断两棵二叉树是否相同:包括中序的两种遍历顺序才能唯一的确定一棵二叉树,因此判断两种遍历的串是否均相同即可判断两棵树是否相同。

数论

结论!

(a*b)%c = (a%c * b%c) %c = (a * b%c) %c = (a%c * b) %c

(a+b)%c = (a%c + b%c) %c = (a + b%c) %c = (a%c + b) %c

只有加 乘  满足  分配率

(x/y) % z != (x%z) /  y

x % a % b != x % b % a

注意!

long long 类型输入输出一定要用%lld!不然出错

1. 数位分解

    依次求模(%10)求商;直接用字符串

2. 进制转换

    特殊的数位分解,先转换到10进制(求积求和),再转换到k进制(求模求商%k)

    可使用C++ reverse(begin, end)函数

    记得用do while!!!保证0也能被算进去

    if(str[i]>='A' && str[i]<='Z') t=str[i]-'A'+ 10;

3. 最大公约数GCD

    欧几里得算法:a、b的公约数同时也必是b、a mod b的公约数。不断重复该过程,直到问题缩小到某个非零数与零的最大公约数,则该非零数即为所求。(将问题转换为规模更小的问题)

算法过程:若a、b全为零则它们的最大公约数不存在;若a、b其中之一为零,则它们的最大公约数为a、b中非零的那个;若a、b都不为零则使新a = b, b = a % b,然后重复该过程。

4. 最小公倍数LCM

    a、b两数的最小公倍数为两数的乘积除以它们的最大公约数。因此将求LCM统一到求GCD上

5. 素数筛法

    朴素方法:用所有大于1小于sqrt(n)的整数去试着整除n,若在该区间内存在某个数能整除该数则该数不是素数,否则是素数。int bound = (int)sqrt(x) + 1;//计算枚举上界,防止double值的损失,宁愿多枚举也不少枚举!!!而且保证sqrt只运算一次,sqrt很费时。适用于求一个数是不是素数。

    素数筛法:若一个数不是素数,则必存在一个小于它的素数为其的因数。算法:从2开始遍历2到n的所有整数,若当前整数没有因为它是某个小于其的素数的倍数而被标记成非素数,则判定其为素数,并标记它所有的倍数为非素数。适用于求小于某个数的所有素数。

    当我们判定i为素数,要标记其所有倍数为非素数时,我们并没有从2 * i开始标记,而是直接从i * i开始标记。其原因是显然的,i * k (k < i)必已经在求得k的某个素因数(必小于i)时被标记过了,即i*k同时也是k的素因数的倍数。

6. 分解素因数

    先使用素数筛法预先筛选出所有可能在题面所给定的数据范围内成为素因数的素数。注意只求sqrt(n)范围内的素数即可。并在程序输入待处理数字 n 时,依次遍历所有小于 n 的素数,判断其是否为 n 的因数。若确定某素数为 n 的因数,则通过试除(即不断辗转相除直到不能整除)确定其对应的幂指数。最后求出各个幂指数的和即为所求。 若最后不能整除后n大于1,则说明有大于sqrt(n)的因数即为当前的n,且其幂指数一定为1(不然相乘将大于n)

    例题:给定 n,a 求最大的 k,使 n!可以被 a^k 整除但不能被 a^(k+1)整除。

    输入:两个整数 n(2<=n<=1000),a(2<=a<=1000)

    输出:一个整数.

    样例输入:6 10

    样例输出:1

    解析:用上面的思路会超时而且n!会很巨大。思考。①若整数a能整除b,对a,b分解素因数,则对a中每个素因数b中都有对应且对应幂次不小于 a 中的幂次。则要确定最大的非负整数 k,使 a 中任一素因数的幂指数的 k 倍依旧小于或等于该素因数在 b中对应的幂指数。要求得该 k,我们只需依次测试 a 中每一个素因数,确定 b 中该素因数对应的幂指数是 a 中幂指数的几倍(利用整数除法),这样所有倍数中最小的那个即为我们要求的 k。a 的素因子用上面的方法即可求得,那么下面的问题就是如何求n!的素因子。②n! 中包含了 1 到n 区间内所有整数的乘积,这些乘积中每一个 p 的倍数(包括其本身)都将对 n! 贡献至少一个 p 因子,且我们知道在 1 到 n 中 p 的倍数共有 n/p(整数除法)个,则 p 的因子数至少为 n/p 个,即有 n/p 个整数至少贡献了一个 p 因子。至少贡献2个、3个p因子的算法同理。由于是n的阶乘,因此p一定在小于n的素数中,因此只需分解n的素因子p,再对每个素因子求幂次。通过这种方法可以求得n的所有的素因子和其幂次。

7. 二分求幂即快速幂

    A^B的后x三位数只与A的后x位数和B有关!!!

    在指数层面即分解B为若干个2^k的和

8. 高精度整数

    JAVA使用BigInterger类,C+自己写结构体,按照运算法则定义各类运算,可使用整形数组,一个位表示四位。

    若只是求一个高精度整数除以一个小整数的余数,则可: (原理:取余运算的性质)

int ans = 0; 其中高精度大整数由高位至低位保存在字符数组str中,小整数保存在mod中
for (int i = 0;str[i];i ++) {
ans *= 10;
ans %= mod;
} printf("%d\n",ans); //ans即为计算后剩下的余数

    不能忘记复杂度

图论

1. 并查集

并查集是集合的基本操作,我们用一棵树上的结点来表示在一个集合中的数字,要判断两个数字是否在一个集合中,我们只需判断它们是否在同一棵树中。那么我们使用双亲结点表示法来表示一棵树,即每个结点保存其双亲结点,可以使用数组实现。要合并两个集合我们只需要合并两棵树。由于我们对集合的操作主要通过查根节点实现,因此要找到根节点需要不断查找双亲结点直到结点不存在,该时间和树高有关,因此我们可以在查找某个特定结点的根结点时,同时将其与根结点之间所有的结点都直接指向根结点,这个过程被称为路径压缩。

递归代码如下:

int findRoot(int x) {
if (Tree[x] == -1) return x;
else {
int tmp = findRoot(Tree[x]);
Tree[x] = tmp; //将当前结点的双亲结点设置为查找返回的根结点编号
return tmp;
}
}

求连通分量的个数即可使用并查集,找到集合的个数即可。

2. 最小生成树

如果存在一个连通子图包含原图中所有的结点和部分边,且这个子图不存在回路,那么我们称这个子图为原图的一棵生成树。在带权图中,所有的生成树中边权的和最小的那棵(或几棵)被称为最小生成树。

最小生成树可使用Kruskal算法:

1. 初始时所有结点属于孤立的集合。
2. 按照边权递增顺序遍历所有的边,若遍历到的边两个顶点仍分属不同的集合(该边即为连通这两个集合的边中权值最小的那条)则确定该边为最小生成树上的一条边,并将这两个顶点分属的集合合并。
3. 遍历完所有边后,原图上所有结点属于同一个集合则被选取的边和原图中所有结点构成最小生成树;否则原图不连通,最小生成树不存在。
如步骤所示,在用Kruskal算法求解最小生成树的过程中涉及到大量的集合操作,我们恰好可以使用上一节中讨论的并查集来实现这些操作。实现时,按照边权值增序遍历所有的边,然后判断两边是否属于同一集合即可。

3. 最短路

几种最短路算法简析(详细可查):

Floyd-多源最短路、dp思想:D{K}[i][j] = min{ D{K-1}[i][j], D{K-1}[i][k], D{K-1}[k][j] }

Dijkstra-单源最短路(多源可遍历)、贪心思想、不能计算负权值:每次用新选入集合K的点更新剩余点的最短路径

Bellman-可以计算负权值

SPFA-队列优化的bellman

4. 拓扑排序

选择一个入度为0的结点,作为序列的第一个结点。当该结点被选为序列的第一个顶点后,我们将该点从图中删去,同时删去以该结点为弧尾的所有边,得到一个新图。那么这个新图的拓扑序列即为原图的拓扑序列中除去第一个结点后剩余的序列。同样的,我们在新图上选择一个入度为0的结点,将其作为原图的第二个结点,并在新图中删去该点以及以该点为弧尾的边。这样我们又得到了一张新图,重复同样的方法,直到所有的结点和边都从原图中删去。若在所有结点尚未被删去时即出现了找不到入度为0的结点的情况,则说明剩余的结点形成一个环路,拓扑排序失败,原图不存在拓扑序列。

用于活动安排、判断是否为有向无环图

搜索

1. 枚举

即暴力,搜索空间越大时间复杂度越高,例如百鸡问题。

1. BFS

可用在图的遍历。用于搜索时,一般可转换为搜索树,要点是确定状态空间和状态转换,步骤如下:

1. 状态。我们确定求解问题中的状态。通过状态的转移扩展,查找遍历所有的状态,从而从中寻找我们需要的答案。
2. 状态扩展方式。在广度优先搜索中,我们总是尽可能扩展状态,并先扩展
得出的状态先进行下一次扩展。在解答树上的变现为,我们按层次遍历所有状态。
3. 有效状态。对有些状态我们并不对其进行再一次扩展,而是直接舍弃它。因为根据问题分析可知,目标状态不会由这些状态经过若干次扩展得到。即目标状态,不可能存在其在解答树上的子树上,所以直接舍弃。
4. 队列。为了实现先得出的状态先进行扩展,我们使用队列,将得到的状态依次放入队尾,每次取队头元素进行扩展。
5. 标记。为了判断哪些状态是有效的,哪些是无效的我们往往使用标记。将已经搜索过的进行标记剪枝
6. 有效状态数。问题中的有效状态数与算法的时间复杂度同数量级,所以在进行搜索之前必须估算其是否在我们所可以接受的范围内。

7. 最优。广度优先搜索常被用来解决最优值问题,因为其搜索到的状态总是按照某个关键字递增,这个特性非常适合求解最优值问题。所以一旦问题中出现最少、最短、最优等关键字,我们就要考虑是否是广度优先搜索。
8. 若当前解已不可能是最优解,则剪枝。

2. 递归

最经典的递归->汉诺塔问题,由于每次都只能小盘落在大盘上面,因此移动N个盘子,必定是先将N-1个盘子移动到中间柱,再移动一次最底层的盘子,最后再将N-1个盘子移动到目标柱。因此F(N) = 2F(N-1)+1,而F(1)=1。

递归用途:1. 根据递归递推规则同时考虑递归边界。2. 枚举遍历回溯 3. 是dfs、动态规划等算法的基础

3. DFS

dfs缺少广度搜索中按层次递增顺序遍历的特性。所以当深度优先搜索搜索到我们需要的状态时,其不再具有某种最优的特性。所以,在使用深度优先搜索时,我们更多的求解有或者没有的问题,即对解答树是否有我们需要的答案进行判定,而一般不使用深度优先搜索求解最优解问题。不需要队列,直接递归即可。

动态规划DP

在众多方案中求解最优解,是典型的动态规划问题。

1. 递推求解

同时给n个网友每人写了一封信,把所有的信都装错了信封,一共有多少种可能的错误方式?

错排公式 F[N]=(N-1)*F[N-1]+(N-1)*F[N-2],设想第N个信封装错,设N中装的是K,则K中要么是N要么不是N。若K中是N,则交换K\N后其他都是错的,等于F[N-2],由于K有N-1个取值,乘N-1;另外一种情况类似。

2. 最长递增子序列

找到递推关系:

F[1]=1

F[i]=MAX(1, F[j]+1 | j<i && ai>=aj)

即等于前面所有符合条件的最长递增子序列的最大值+1。

3.最长公共子序列

递推条件:

dp[0][i]=0, dp[j][0]=0 (0<=i,j<=m)

dp[i][j]=dp[i-1][j-1]+1 (s1[i]==s2[j])

dp[i][j]=max(dp[i][j-1], dp[i-1][j] (s1[i]!=s2[j])

dp[i][j]代表以s1[i]为结尾的子串和s2[j]为结尾的子串的最长公共子序列

若s1的第i个字符和s2的第j个字符如果相等,即等于以s1[i-1]为结尾的子串和s2[j-1]为结尾的子串的最长公共子序列+1;若不等,则等于以s1[i]为结尾的子串和s2[j-1]为结尾的子串的最长公共子序列以及以s1[i-1]为结尾的子串和s2[j]为结尾的子串的最长公共子序列中最大的一个。

4. 状态转移方程

DP是通过对状态的不断求解最终得到答案的。状态是描述问题当前状况的一个数字量。首先,它是数字的,是可以被抽象出来保存在内存中的。其次,它可以完全的表示一个状态的特征,而不需要其他任何的辅助信息。最后,也是状态最重要的特点,状态间的转移完全依赖于各个状态本身。

DP最关键的即使决定状态和状态转移方程,将问题分解为相互有重合的子问题,在求解时对已经求解的子问题不再重复计算。

动态规划问题的时间复杂度由两部分组成:状态数量和状态转移复杂度,往往程序总的复杂度为它们的乘积。

5. 0-1背包

朴素的0-1背包:使用dp[i][j]表示在总体积不超过j的情况下,前i个物品所能达到的最大价值。初始时,dp[0][j](0<=j<=V)为0。依据每种物品是否被放入背包,每个状态有两个状态转移的来源。若物品i被放入背包,设其体积为w,价值为v,则dp[i][j] = dp[i - 1][j - w] + v。即在总体积不超过j-w时前i-1件物品可组成的最大价值的基础上再加上i物品的价值v;若物品不加入背包,则dp[i][j] = dp[i-1][j],即此时与总体积不超过j的前i-1件物品组成的价值最大值等价。选择它们之中较大的值成为状态dp[i][j]的值。状态转移方程为:dp[i][j] = max{dp[i - 1][j - wi] + v, dp[i-1][j]}。初始时,dp[0][j](0<=j<=V)为0。

0-1背包的空间优化:dp[i][j]的转移仅与dp[i-1][j-list[i].w]和dp[i-1][j]有关,即仅与二维数组中本行的上一行有关。根据这个特点,我们可以将原本的二维数组优化为一维,改变状态方程为:dp[i][j] = max{dp[j - wi] + v, dp[j]}。为了保证dp[j]和dp[j-list[i].w]尚未被本次更新修改,可以在每次更新中倒序遍历所有j的值。

for (int i = 1;i <= n;i ++) {
    for (int j = s;j >= list[i].w;j --) { //必须倒序更新每个dp[j]的值,j小于list[i].w的各dp[j]不作更新,保持原值,即等价于dp[i][j] = dp[i - 1][j]
        dp[j] = max(dp[j],dp[j - list[i].w] + list[i].v); //dp[j]在原值和dp[j - list[i].w]+list[i].v中选取较大的那个
    }
}

要求所选择的物品必须恰好装满背包:设计新的状态dp[i][j]为前i件物品恰好体积总和为j时的最大价值,其状态转移与前文中所讲的0-1背包完全一致,而初始状态发生变化。其初始状态变为,dp[0][0]为0,而其它dp[0][j](前0件物品体积总量为j)值均变为负无穷或不存在,经过状态转移后,得出dp[n][s]即为答案。其他步骤不变。区分是否能恰好装满,只需要看最后一个状态的值是不是正常范围内的值即可。

完全背包:每种物品的数量为无限个。法1:可以将该物品拆成V/w件,即将当前可选数量为无限的物品等价为V/w件体积为w、价值为v的不同物品。对所有的物品均做此拆分,最后对拆分后的所有物品做0-1背包即可得到答案。但是复杂度较高。法2:利用空间优化过的的一维数组,按正序进行状态转移。之前的逆序循环,保证了更新dp[j]时,dp[j - list[i].w]是没有放入物品i时的数据(dp[i - 1][j - list[i].w]),这是因为0-1背包中每个物品至多只能被选择一次。而在完全背包中,每个物品可以被无限次选择,那么状态dp[i][j]恰好可以由可能已经放入物品i的状态dp[i][j - list[i].w]转移而来,固在这里将状态的遍历顺序改为顺序,使在更新状态dp[j]时,dp[j - list[i].w]时可能因为放入物品i而发生改变,从而达到目的。

for (int i = 1;i <= n;i ++) {
    for (int j = list[i].w;j <= s;j ++) {
        dp[j] = max(dp[j],dp[j - list[i].w] + list[i].v);
    }
}

多重背包:有容积为V的背包,给定一些物品,每种物品包含体积w、价值v、和数量k,求用该背包能装下的最大价值总量。可以直接转化到0-1背包,降低每种物品的数量ki将会大大的降低其复杂度,于是将原数量为k的物品拆分为若干组,每组物品看成一件物品,其价值和重量为该组中所有物品的价值重量总和,每组物品包含的原物品个数分别为:为:1、2、4…k-2^c+1,其中c为使k-2^c+1大于0的最大整数。这种类似于二进制的拆分,不仅将物品数量大大降低,同时通过对这些若干个原物品组合得到新物品的不同组合,可以得到0到k之间的任意件物品的价值重量和,所以对所有这些新物品做0-1背包,即可得到多重背包的解。

附录1. string常用:

① string中find()的用法 rfind (反向查找) 

(1)size_t find (const string& str, size_t pos = 0) const;  //查找对象--string类对象

(2)size_t find (const char* s, size_t pos = 0) const; //查找对象--字符串

(3)size_t find (const char* s, size_t pos, size_t n) const;  //查找对象--字符串的前n个字符

(4)size_t find (char c, size_t pos = 0) const;  //查找对象--字符

结果:找到 -- 返回 第一个字符的索引

没找到--返回   string::npos(代表 -1 表示不存在)

② string中substr的用法

string substr (size_t pos = 0, size_t len = npos) const; 产生子串

返回一个新建的初始化为string对象的子串的拷贝string对象。

子串是,在字符位置pos开始,跨越len个字符(或直到字符串的结尾,以先到者为准)对象的部分。

③string中append的用法

直接添加另一个完整的字符串:

str1.append(str2); str1.append("abc"); str1.append("a");

或者:如果想在string s末尾追加abc

s = s + 'a' + 'b' + 'c'; //这样写是可以的  
s += 'a' + 'b' + 'c'; //这样写是不对的  

添加另一个字符串的某一段子串:

str1.append(str2, 11, 7);

添加几个相同的字符:

str1.append(5, '.');

④str.c_str() 可输出%s的C风格的字符串。

⑤s.erase (10,8) 从string对象str中删除从s[10]到s[17]的字符,即从s[10]开始的8个字符。

⑥a.insert(2,b) 在a中下标为2的字符前插入b字符串。其中string对象b也可以为字符数组。

2. 迭代器用法:

容器::iterator iter;

for(iter= 容器.begin();iter!=容器.end();iter++){

cout<<*iter或者是 iter->first等等之类的                                    //迭代器就是这么个套路

}



  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值