习题课2-1
排序
解法1 选择排序
-
for(int i=0;i<n;i++){ int minEle = a[i]; int tmp; for(int j=i+1;j<n;j++){ minEle = Math.min(minEle,a[j]); tmp = j; } a[j] = a[i]; a[i] = minELe; }
-
选择排序:每次选出一个最小的放在前面
-
每次都要全部扫一遍,
-
复杂度一直都是O(n^2)
-
每次扫描,用一个额外变量记录最小元素的下标
-
扫描完毕,通过记录的最小元素下标,将初始最小元素和记录的最小元素交换。
解法2 插入排序
-
for(int i=0;i<n;i++) for(int j=i;j>0;j--){ if(a.get(j)<a.get(j-1) collections.swap(a,j,j-1); else break; }
-
插入排序:每次将元素插入到合适的位置
-
最坏的情况是将一个倒序的数组排序为正序,此时就是O(n2)的
-
保证游标走到位置之前都是有序的,插入一个新元素时,会前缀有序序列的末尾开始往前逐一比较,如果小于就交换,不小了就直接break,因为已经是有序排列的
解法3 冒泡排序
-
for(int i=1;i<n;i++) for(int j=0;j<n-i;j++){ if(a.get(j)>a.get(j+1) Collections.swap(a,j,j+1); }
-
冒泡排序:每一次将最大的泡冒到最后,刚好与选择排序相反
-
i从1开始,是防止j+1越界
解法4 归并排序
-
void sort(int l,in r){ if(l==r)//只有一个元素了,不需要操作了 return; int mid = (l+r)>>1; sort(l,mid); sort(mid+1,r); int l_idx = l; int r_idx = mid+1; int b_idx = l; while(l_idx<=mid && r_idx<=r) if(a(l_idx)<a(r_idx) b[b_idx++] = a[l_idx++]; else b[b_idx++] = a[r_idx++]; while(l_idx<=mid) b[b_idx++] = a[l_id++]; while(r_idx<=r) b[b_idx++] = a[r_idx++]; for(int i=l;i<=r;i++) a[i] = b[i]; }
-
归并排序:递归排序左右两半,再合并
-
递归的排序(实际是一个后序遍历)
-
切分的前后两半数量不超过1
-
第一步是考虑边界情况
-
将数组切分成两半,找到中间值,递归排序左右两半
-
再合并排序好的两部分
-
b[]是一个新数组,b_idx是新数组的下标
-
首先同时从左右两半中取出更小的数放到b数组中,此时始终只会有一个数组全部放到了b数组中
-
所以之后需要判断左右两个数组的下标,判断是否全部放到b数组中了,如果没有继续往b数组中填充
-
最后将新排序好的数组重新赋值给原数组a
-
解法5 快速排序
-
void sort(int l,int r){ if(l>=r) //l是向右走的,r是向左走的 return; int bound = a[rand(l,r)]; //取一个随机的数 int l_idx = l; int r_idx = r; while(l_idx<=r_idx) { //必须有等于,不然左右下标相等就不动了,这样递归下去左右两半就有了重复的部分 // 下面必须是小于,因为等于和小于的界桩值都放在左边,而此时的如果这两个值是3,2,就没有起到排序的作用,保证了l_idx的左边的数全部都是小于界桩值的 while(a[l_idx]<bound) //从左边找到第一个比界桩大的值 ++l_idx; while(bound<a[r_idx]) //在右边找到第一个比界桩小的值 --r_idx; if(l_idx<=r_idx) //与while循环中的等号是一样的,如果没有等号,递归时可能是重复的 swap(a[l_idx++],a[r_idx--]);//交换左右两个值的同时,左下标+1,右下标-1 } // while执行完后,l是大于r的,大1或者大2 sort(l,r_idx); sort(l_idx,r); }
-
快速排序:随机找一个数作为界值,然后将序列划分成左右两端递归(前序遍历)
-
一定要注意快速排序的边界情况
-
找界桩值pivot,使的pivot前面都比pivot小,pivot后面的元素都比pivot大
-
切分数组,得到一个下标j,此时的j就是第一条所说的pivot,然后对(lo,r_pivot)和(l_pivot,hi)递归排序即可
-
改进的点在于界桩值一般是取a[0],此处取的随机值,有可能直接选中了界桩值
-
默认不支持开栈,默认栈空间是8M
补充
堆排序
- 选择排序用堆这种数据来存储。
拓扑排序
- 有向带权图(只有一个入度为1的点)
桶排序(基数排序)
- 哈希表
- 质数取模
分组
-
简要题意:n个正整数,分成连续m份,要求最大的那一份的数字之和尽量小。
-
最大的最小,看到这个关键字,就要想想二分能不能做。
-
转化题意,保证每份之和不超过d,最少能分成多少份,设这个份数是cnt
-
如果cnt<=m,并且n>=m的,所以一定是可以划分成m份的,而当cnt>m了,则不能划分成m份
-
二分答案
-
主函数
-
// 返回值:最优方案中,数字之和最大的那一份的数字之和 long long getAnswer(int n,int m,vector<int> a){ // l表示答案下界,r表示答案上界 long long l = 1, r = 0; // 求出初始上界r // 分成1份,所有的数归成1份 // 不能有负数,不然这样求出的上界就不是最大的了 for(int i=0;i<n;i++){ r += a[i]; } // 二分答案 while(l<=r){ long long mid = (l+r) >> 1; // 验证mid可不可能为答案,如果可能,上界缩小 // 因为mid满足了,就可以去(l,mid-1)的区间里去找尽可能小的d值 if(check(mid,n,m,a)) r = mid -1; else // 否则下界增大 l = mid + 1; } // 因为最后一次check满足的时候,r = mid -1 // 而mid一直在变化,所以只能倒推出mid = r + 1; return r+1; //答案为r+1,因为r+1是最后一个满足check的mid的值 }
-
进行二分求解
-
为什么题目满足二分答案?
-
具有单调性,d=6可分成m份,则d+1、d+2、…都可以划分成m份,
-
反过来,如果d=5不可以分成m份,所以d-1、d-2、…也都不可以划分m份
-
可以拆分成两部分
-
画成01曲线,best的位置就是0突变成1的地方
-
通过二分可以最终找出best
-
检查是否可以分成m份
-
// 若能分成不超过m份,返回true,否则返回false bool check(long long d,int n,int m,vector<int> &a){ long long sum = 0;// 用于记录当前那一份的数字之和 int cnt = 1;// 在每一份数字之和不超过d的情况下,至少要分成的份数 for(int i=0;i<n;i++){ if(a[i]>d) // 如果单个元素大于了d,直接返回false return false; sum += a[i]; if(sum>d){ // 将a[i]加入当前这一份中 sum = a[i]; // 将a[i]单独拿出来作为新的一组的开头 cnt = cnt +1; // 组数加1 } } return cnt<=m; }
-
小技巧,看到”最大的最小“这样的描述或者类似的描述,转换成二分查找。
大转盘
- 简要题意,给定整数n(1<=n<=16),有一个切成了2的n次方的圆盘,每块有一个数组(0或者1)。从第i个格子出发,顺时针走过n个格子,依次读出n个数字形成一个二进制数m
- 构造一个圆盘,满足这些m形成一个0到2的n次方-1的排列
- 下图中是n等于4的情况
- 比如0010,然后转盘转动变成010,最后一位可以是0或者1,
- 发现所有的点分别是000,001,010,011,100,101,110,111
- 当001,添加一个0,则走到010这个点
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cm1uDCPF-1605714344228)(C:\Users\liusiping\AppData\Roaming\Typora\typora-user-images\image-20201001120041033.png)]
- 发现每个点的入度和出度都等于2
- 结论:每个点的入度等于出度,就存在欧拉回路
- 每走一条边,添加一个数字,走完所有的点,就是一条欧拉回路
- 离散数学
解法1
- 蛮力算法
- 枚举0位置开始的数字,然后进行搜索(dfs)
- 下一个位置的数字只有两种选择,在另一头加上0或者在另一头加上1
- 若发现有重复的就不再深入往下搜索,怎么发现重复,用set记录
解法2
-
我们将2的n次方二进制数看出一条边,一条边可能和一些边有关系,这个关系我们看成一个节点
-
而这个关系就是一个n-1位的二进制数,这个“关系”恰好有两条边连入,有两条边连出,(对应着一头删掉,另一头加上)
-
怎么理解,只看边,不看点,连接边的两个点刚好是变化的左右端点(0或1)
-
而对这个建成的图每条边遍历一遍,就是答案
-
每个点恰好有两条入边,两条出边,此时就存在欧拉回路
-
void dfs(int u){ for(int i=0;i<2;i++){ if(!vis(i)(u)){ // 将u左移一位,然后将最低位置置为1,再将最高位去掉 int v = ((u<<1)|i) & allOne; vis(i)(u) = 1; // 递归v,加入数字到ans中 dfs(v); // 先递归,再加入,所以最后得到的排列是一个逆序结果 // 为什么先递归,是为了标记好找到每一条边的顺序 ans.push_back('0'+i); } } }
-
int allOne;// 全1,防止乘2越界 vector<bool> vis[2];// vector的长度是欧拉图中点的数量,而每个点往下一个点移动都有0,1两种选择 string ans; int twoPow(int x){ return 1<<x; } String getAnswer(int n){ allOne = twoPow(n-1) -1; ans = ""; for(int i=0;i<2;i++) vis[i].resize(twoPow(n-1),0); dfs(0);// 从0节点开始,搜出一条欧拉回路 return ans;// 因为是圆盘,无论顺序还是逆序都是对的 }
-
怎么构建一条边
- ((u<<1)|i) &a
- u<<1,相当于去掉首位
- 再|i, 此处的i可能为0和1,表明了出边为两条,
- &a,是对节点大小的限制,当节点是111这个量级时,保证不会因为<<1的操作而越界。
解法3
-
提交答案
-
if(n==1) cout << "1"; if(n==2) cout << "2"; .... 因为总共n是小于等于16