可能的填空
算法是什么
算法是求解问题的一系列计算步骤
算法设计要求
- 正确性
- 可使用性(用户友好)
- 可读性
- 健壮性(容错)
- 高效率和低存储率
算法特性
- 有限性
- 确定性
- 可行性
- 输入
- 输出
程序
程序=数据结构+算法
数据结构
- 逻辑结构
- 存储结构
- 基本操作
数据结构是算法的基础
算法设计步骤
算法分析
时间复杂度
logn
for (int i=1; i<=n; i*=2) {}
注意,有2的要注意,大概是logn
空间复杂度
硬件
- CPU
- 存储
- 传输
软件
- 操作系统
- 编译器
- 编程语言
递归
何时使用递归
- 定义是递归的
- 数据结构是递归的
- 求解方法是递归的
递归种类
- 直接递归
- 间接递归
- 尾递归
递归模型的组成*
- 递归体
- 递归出口
递归问题的特点
- 可以转化成一个或者多个子问题
- 递归调用的次数必须是有限的
- 必须有结束递归的条件
递归算法设计
- 问题分析,给出大问题和小问题的定义
- 确定递归体
- 确定递归出口
- 给出算法
例题
阶乘
int jc (int n) {
if (n == 1) return 1;
return n * jc(n-1);
}
斐波那契
int fb (int n) {
if (n == 1 || n == 2) return 1;
return fb(n-1) + fb(n-2);
}
求数组最大值
int maxElem(int i, int j) {
int mid = (i+j)/2;
if (i < j) {
max1 = maxElem(i, mid);
max2 = maxElme(mid+1,j);
return max1>max2 ? max1 : max2;
}
}
int fmax(int i) {
if (i == 1) return a[0];
return max(fmax(i-1),a[i-1]);
}
所有数字之和
vector<int> vi;
int sum = 0;
void sumE(int x) {
if (x == vi.size()-1) {
sum += vi[x];
} else {
sum += vi[x];
sumE(x+1);
}
}
链表销毁
void del(LinkList & node) {
if (node != null) {
del(node->next);
//free(node);
delete node;
}
}
二叉树先序中序后序
void xx(LinkList &node) {
if (node != null) {
//print node->val
xx(node->left);
xx(node->right);
}
}
void zx(LinkList &node) {
if (node != null) {
xx(node->left);
//print left
xx(node->right);
}
}
void hx(LinkList &node) {
if (node != null) {
xx(node->left);
xx(node->right);
//print right
}
}
输出每一位(n进制)
int ans = 0;
void rec(int x, int base) {
if (x > 0) {
ans += x%base;
rec(x/base);
} else {
return ;
}
}
汉诺塔
void Hanoi(int n, char x, char y, char z) {
if (n == 1) {
//print
} else {
Hanoi(n-1, x, z, y);
//print
Hanoi(n-1, y, x, z);
}
}
n皇后
一个函数判断该点能不能放place
一个函数放置
void queen (int i, int n) {
if (i > n) dispasolution(n);
else {
for (int j=1; j<=n; j++) {//j列合法
if (place(i,j)) {
q[i] = j;
queen(i+1,n);
}
}
}
}
问四皇后两个解是什么?
第k大
//可以用优先队列priority_queue
//排序然后选出
//二分找
字符串长度
int len = 0;
string s;
void fun (int x) {
if (s[x] == '\0') return ;
len++;
fun(x+1);
}
int fun(int x) {
if (ch[x] == '\0') return 0;
return dfs(x+1)+1;
}
分治
- 排序(快排,归并)
- 查找(二分)
- 组合
- 乘法
求解过程*
- 分解原问题
- 求解子问题
- 合并子问题
例题
归并*
nlogn
把数组一直划分到剩下两个数字,然后针对每个组进行内部的排序,最开始组内只有1个数字,然后两个组两个组进行合并,再次进行排序,以此类推,最后从n个组,合并成1个组,即完成排序
void merge(int l, int r) {
int mid = (l + r) >> 1;
int i = l, j = mid + 1;
int cnt = l;
while (i <= mid && j <= r) {//只要两个组里面都还有数字那就继续
if (arr[i] < arr[j]) {
temp[cnt++] = arr[i++];
} else {
temp[cnt++] = arr[j++];
}
}
while (i <= mid) temp[cnt++] = arr[i++];//如果左边的组里还有,那就全部弄到temp数组里
while (j <= r) temp[cnt++] = arr[j++];//右边同上
for (int k=l; k<=r; k++) {//最后将临时数组temp的值传给arr
arr[k] = temp[k];
}
}
void quick(int l, int r) {
if (l < r) {
int mid = (l + r) >> 1;
quick(l, mid);//划分左右两个组
quick(mid+1, r);
merge(l, r);//划分完之后进行合并
}
}
快排
nlogn
- 从数字中挑出一个基准数字
- 把基准数字左边和右边的数字进行划分,即比基准数字小的数字放基准数字左边,大的放右边
- 然后分治处理,在每一个基准分出的左边和右边都进行这样的操作
void quick_sort(int q[], int l, int r) {//y总模板
if (l >= r) return;
int x = q[l + r >> 1], i = l - 1, j = r + 1;
while (i < j) {
do i ++ ; while (q[i] < x);
do j -- ; while (q[j] > x);
if (i < j) swap(q[i], q[j]);
}
quick_sort(q, l, j);
quick_sort(q, j + 1, r);
}
求最大元素和次大元素
二分或者递归求解两个值
二分查找!!!
public int binarySearch(int[] a, int start, int end, int target) {
if(start > end)
return -1;
int mid = start + (end - start) / 2;
if(a[mid] == target)
return mid;
else if(a[mid] > target)
return binarySearch(a, start, mid-1, target);
else
return binarySearch(a, mid+1, end, target);
}
第k小元素
二分
最大连续子序列和(组合问题)!!!
棋盘覆盖!!!
大整数乘法(分治)
略
矩阵相乘(矩阵快速幂)
略
蛮力
例题
完全数
各因子加起来等于本身6=1+2+3
字符串匹配!!!
选择排序
挑最小元素到最前面
插入排序
无序插入有序
最大连续子序列和!!!
有题
幂集
给定正整数,求1-n构成的集合的所有子集
例如:{1,2,3}的幂集是{},{1},{2},{3},{1,2},{1,3},{2,3},(1,2,3)
有 2 n 2^n 2n个
转化为二进制串求解
写法:共有 2 n 2^n 2n的数量
那么遍历从1到 2 n 2^n 2n(也可以用数组来存),再判断单个位数即可
全排列(任务分配)
123,132,312,213,231,321
时间复杂度 n 2 n^2 n2
增量法
用vector的insert解决,语法:insert(vi.begin(),i)
组合 递归
图的遍历
dfs,遍历即可,注意限制条件
bfs,注意标记,限制条件以及访问相邻结点
回溯
名词解释
一个问题的解可以表示成解向量
解空间:所有取值的组合构成问题的解向量空间,成为解空间
解空间树:解空间一般组织成树形结构,因此被成为解空间树
入队形成活结点,拿出形成拓展结点,死结点就是不在拓展
回溯和dfs的异同*
- 访问次序不同
dfs目的是遍历,回溯目的是回溯
- 访问次数不同
dfs对访问的结点不再访问,回溯访问过的可能会再访问
- 剪枝不同
dfs不含剪枝,很多回溯有剪枝
例题
装载问题
和01背包差不多,就是等于
子集和
还是差不多,就是递归结束特判一下
分枝限界
分枝限界和回溯的异同*
- 在解空间树搜索问题的解
- 两种算法均能找到一个解,所有解和最优解
区别于回溯法,搜索方式不同:回溯是dfs,分枝限界是bfs
目标不同:
- 回溯:找出所有解
- 分枝限界:找出一个可行解或者某种意义上最优解
都属于穷举法,最坏情况下时间复杂度都说指数阶
栈实现和队列实现
分枝限界的优点和缺点
可以更快的找到一个解或者最优解,缺点是要存储结点的限界值,占用内存较多
求解效率由限界函数决定,极端情况下和穷举无大区别
贪心
例题
活动安排
截止时间排序
集装箱
排序直接写
背包
错误解法,价值排序
动态规划
动态规划和分治法的区别
分治:子问题是独立的,自顶向下
dp:子问题重叠,自底向上
名字解释
描述决策过程当前特征的量称就是状态
决策就是决策者在过程中处于某一阶段的某一状态时对面下一阶段的决定或者选择
某一状态以及该状态下的策略,与下一状态之间指标函数的关系,被称为状态转移方程
01背包专题
蛮力法
和幂集一样把可能列出来,然后所有情况都计算重量和价值,把重量符合的价值又最高的记录下来即可
回溯法
解向量就是[1,0,1]之类的,表示第几步的选择
约束条件:拓展结点剪除不满足约束的子树
限界条件:剪去得不到问题解或者最优解的子树
解空间树为子集树:O( 2 n 2^n 2n)
解空间树为排列树:O(n!)
要看题意,如果是恰好是W,那么就需要改
int tw, tv, rw;
int op[1099];
int rv[1099];
void dfs(int cen) {
if (cen > n) {
if (tw <= W && tv > maxv) {
maxv = tv;
for (int i=0; i<n; i++) {
ans[i] = op[i];
}
}
} else {
op[i] = 1;
if (tw + w[i] <= W) {
dfs(cen+1);
}
op[i] = 0;
dfs(cen+1);
}
}
需要先算出rv
int tw, tv, rv;
int op[1099];
int rv[1099];
void init() {
int sum = 0;
for (int i=0; i<n; i++) sum += v[i];
for (int i=0; i<n; i++) {
sum -= v[i];
rv[i] = sum;
}
}
void dfs(int cen) {
if (cen > n) {
if (tw <= W && tv > maxv) {
maxv = tv;
for (int i=0; i<n; i++) {
ans[i] = op[i];
}
}
} else {
op[i] = 1;
if (tw + w[i] <= W) {
dfs(cen+1);
}
op[i] = 0;
if (tv + rv[i] > maxv) {
dfs(cen+1);
}
}
}
分枝限界
普通队列
ub
上界和之前的rv
差不多
void bound(Node & node) {
int i = node.i + 1;
int maxv = node.v;
int maxw = node.w;
while ((sumw + w[i] <= W) && i <= n) {//表示把sumw和sumv加上w和v,直到sum装不下为止
sumw += w[i];
sumv += v[i];
i++;
}
if (i <= n) {//除了最后一层不用加,其他都要加
node.ub = sumv + (W - sumw)*v[i]/w[i];//W-sumw是剩下的空间,剩下的空间乘以相应的比例
} else {
node.ub = sumv;
}
}
void EnQueue (Node node) {
if (node.i == n) {
if (e.v > maxv) {
maxv = e.v;
for (int j=1; j<=n; j++) {
beatx[j] = e.x[j];
}
}
} else {
qu.push(node);
}
}
void bfs() {
Node a, tm, tm2;
queue<Node> qu;
a.no = ++total;
a.i = 0;
a.w = 0, a.v = 0;
bound(a);
qu.push(a);
while (!qu.empty()) {
a = qu.front();
qu.pop();
if (a.w + w[a.i+1] <= W){//限界,超过重量
tm.no = total++;
tm.i = a.i++;//比父节点多1层
tm.w = a.w + w[tm.i];
tm.v = a.v + v[tm.i];
for (int j=1; j<=n; j++) {
tm.x[j] = a.x[j];
}
tm.x[tm.i] = 1;
bound(tm);
EnQueue(tm);
}
tm2.no = total++;
tm2.i = a.i+1;
tm2.w = a.w;
tm2.v = a.v;
for (int j=1; j<=n; j++) {
tm2x[j] = a.x[j];
}
tm2.x[tm2.i] = 0;
bound(tm2);
if (tm2.ub > maxv) {//剪枝,如果有可能大于才能进入
EnQueue(tm2);
}
}
}
优先队列式
和上面差不多,就是加了一点东西
bool operator < (const Node & s) const {
return ub < s.ub;
}
然后顺序不同,因为根据ub来获取,所以同进度优先队列maxv会比普通队列大,所以会剪枝
分治
dc(n,rw)
动态规划
问题(两个都是)
- dp表的值
- 每个之后dp的表
#include<bits/stdc++.h>
using namespace std;
int w[1099], v[1099];
int dp[1099][1099];
int main() {
int n, vl;
cin>>n>>vl;
for (int i=1; i<=n; i++) {
cin>>w[i]>>v[i];
}
for (int i=1; i<=n; i++) {
for (int j=1; j<=vl; j++) {
if (j < w[i]) {//装不下了
dp[i][j] = dp[i-1][j];
} else {//有i件物品可以拿,j是
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i]);
}
}
}
cout<<dp[n][vl]<<endl;
}
表格按照规律和容量r来写
押题
请用蛮力,回溯,分枝限界,动态规划算法解答01背包问题,并对比贪心算法解答01背包,说说有什么不同?
-
选择10 5*2
-
判断10
-
填空 包含计算时间复杂度10 5*2
-
算法应用60
各种方式解决01背包45 -
算法设计10(可以用伪代码或者自然语言,但是要描述清楚)