- 概念
- 快速排序(先找分界点再递归)
- 归并排序(先递归再合并)
- 快速幂(拓展移位运算,二进制的理解)
- 快速乘
- STL的使用(下标问题和避免SE)
- binary search二分查找(有序序列中查找元素)
- 二分答案(check函数的模拟)
- 分形(找规律)
- 进阶的有,三分,0/1分数规划
- 《一本通》题目
- 【例7.4】 循环比赛日程表">【例7.4】 循环比赛日程表
- 【例7.5】 取余运算(mod)">【例7.5】 取余运算(mod)
- 【例7.6】黑白棋子的移动">【例7.6】黑白棋子的移动
- 【例7.7】光荣的梦想">【例7.7】光荣的梦想
- 2011">2011
- 输出前k大的数">输出前k大的数
- 区间合并">区间合并
- 求排列的逆序数">求排列的逆序数
- 一元三次方程求解">一元三次方程求解
- 统计数字">统计数字
- 查找最接近的元素">查找最接近的元素
- 二分法求函数的零点">二分法求函数的零点
- 网线主管">网线主管
- 月度开销">月度开销
- 和为给定数">和为给定数
- 不重复地输出数">不重复地输出数
- 膨胀的木棍">膨胀的木棍
- 河中跳房子">河中跳房子
概念
分治,将较大规模的问题分解成几个较小规模的问题。
例子:猜数字游戏,从几楼扔下手机不会摔坏,查找C盘中最大的文件,黄页电话簿,翻字典。
快速排序(先找分界点再递归)
// 学习目标:理解好分治的过程,讲解理论用处多,实际应用比较少
void quick_sort(int q[], int l, int r)
{
if (l >= r) return;
int i = l - 1, j = r + 1, x = q[l + 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);
}
例题:输出前k大的数
归并排序(先递归再合并)
// 学习目标:理解过程,实际应用会灵活多变
void merge_sort(int q[], int l, int r)
{
if (l >= r) return;
int mid = l + r >> 1;
merge_sort(q, l, mid);
merge_sort(q, mid + 1, r);
int k = 0, i = l, j = mid + 1;
while (i <= mid && j <= r)
if (q[i] <= q[j]) tmp[k ++ ] = q[i ++ ];
else tmp[k ++ ] = q[j ++ ];
while (i <= mid) tmp[k ++ ] = q[i ++ ];
while (j <= r) tmp[k ++ ] = q[j ++ ];
for (i = l, j = 0; i <= r; i ++, j ++ ) q[i] = tmp[j];
}
例题:【例7.7】光荣的梦想
快速幂(拓展移位运算,二进制的理解)
求模
分治的方法
// 求:b^p
// 思路:
// 任意一个数p,可以表示成 p = 2 * p/2 + p%2
// b^(2 * p/2 + p%2),就可以拆开后,递归调用f(p/2),递归终止边界是p == 0, return 1;
// 求2^b
#include <iostream>
using namespace std;
int f(int b)
{
if (b == 0) return 1;
int t = f(b / 2);
if (b & 1) return t * t * 2;
else return t * t;
}
int main()
{
int a, b;
cin >> b;
cout << f(b) << '\n';
return 0;
}
// 求a^b
#include <iostream>
using namespace std;
typedef long long ll;
int f(int a, int b)
{
if (b == 0) return 1;
ll t = f(a, b / 2);
if (b & 1) return t * t * a;
else return t * t;
}
int main()
{
int a, b;
cin >> a >> b;
cout << f(a, b) << '\n';
return 0;
}
移位运算的方法
根据数学常识,每一个正整数可以唯一表示为若干个指数不重复的2的次幂的和。如果b在二进制下有k位,其中第i位的数字是,那么
于是:
因为向上取整,所以上式乘积项的数量不多于k个,
又因为
所以,通过k次递推求出每个乘积项,当时,把该乘积项累积到答案中。
b & 1 运算可以取出 b 在二进制表示下的最低位, 而 b >> 1 运算可以舍去最低位,在递推的过程中将二者结合,就可以遍历 b 在二进制表示下的所有数字 。时间复杂度是 。
// 求 a^k mod p,时间复杂度 O(logk)
// 模板请记忆
int qmi(int a, int k, int p)
{
int res = 1;
while (k) //当k不是0的时候
{
if (k & 1) res = (LL)res * a % p;
k >>= 1;
a = (LL)a * a % p;
}
return res;
}
例题:取余运算(mod)
输入b,p,k的值,求 b^p mod k 的值。其中b,p,k为长整型数。
例题:1234:2011
通过测试发现,2011的n次方的结果,每500个数一循环。1次方、501次方、1001……结果一样。
2次方,502次方,1002次方……结果一样。所以,尽管指数可能有200位,只需要考虑最后的3位指数就可以了。如果指数n不足3位,直接计算2011的n次方结果mod 10000就行了。
快速乘
快速乘法的说法来源于快速幂,因为两者的思想一致。快速幂是为了计算计算取模结果,的确是快的。而快速乘法主要是模数超过九次方时使用,因为两数相乘会longlong溢出,会比直接乘法慢,但是保证了正确性。所以说快速乘法并不快,想要比正常乘法快也是不现实的,快速乘法本来就是为了保证正确性而牺牲效率,自己实现了一遍乘法。要说快的话只能去和直接累加相比,就像快速幂比直接累乘取模快。
// 类似快速幂
// log(n)
int qmul(int a, int b, int p){
int ret = 0;
while (b){
if (b & 1) ret = (ret + a) % p;
b >>= 1;
a = (a + a) % p;
}
return ret;
}
STL的使用(下标问题和避免SE)
//STL
int pos = lower_bound(a, a + n, x) - a;
int pos = upper_bound(a, a + n, x) - a;
//lower_bound() 函数用于在指定区域内查找不小于目标值的第一个元素。也就是说,使用该函数在指定范围内查找某个目标值时,最终查找到的不一定是和目标值相等的元素,还可能是比目标值大的元素。
//upper_bound() 函数的功能和 lower_bound() 函数不同,前者查找的是大于目标值的元素,而后者查找的不小于(大于或者等于)目标值的元素。
例题:和为给定数
binary search二分查找(有序序列中查找元素)
二分的基础用法是在单调序列或单调函数中进行查找。因此当问题的答案具有单调性时,就可以通过二分把求解转化为判定。其实,写二分的时候并不容易,写check函数的时候,有很多细节之处需要仔细考虑。
- 对于整数域上的二分,需要注意终止边界、左右区分取舍时的开闭请,避免漏掉进入死循环。
- 对于实数域上的二分,需要注意精度问题。
二分的实现模板有很多种,以下给出的模板是yxc的,非常好用。
//模板请记忆
//整数二分
bool check(int x) {/* ... */} // 检查x是否满足某种性质
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid; // check()判断mid是否满足性质
else l = mid + 1;
}
return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
//注意,分析题意,画数轴,看mid的右边是答案,还是mid的左边是答案
//模板请记忆
//浮点数二分,还有循环50次的写法
bool check(double x) {/* ... */} // 检查x是否满足某种性质
double bsearch_3(double l, double r)
{
const double eps = 1e-6; // eps 表示精度,取决于题目对精度的要求
while (r - l > eps)
{
double mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid;
}
return l;
}
double bsearch_4(double l, double r)
{
int T = 50;
while (T--)
{
double mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid;
}
return l;
}
例题:和为给定数
例题:查找最接近的元素
二分答案(check函数的模拟)
一个宏观的最优化问题也可以抽象为函数,其“定义域”是该问题下的可行方案,对这些可行方案进行评估得到的数值构成函数的“值域”。假设评估得到的数值最高值是 s,对于大于 s 的都不合法,对于小于等于 s 的都是合法的,这样问题的值域就有一种特殊的单调性,在 s 的一侧合法,在 s 的另一侧不合法。借助二分,我们把求最优解的问题,转化为给定一个值 mid,判定是否存在一个可行方案评分达到 mid 的问题。
二分答案可以用来处理“最大的最小”或“最小的最大”问题。
常见问题:记录答案的二分写法,判断答案在哪边,check()函数经常写不准确的问题。
例题:网线主管
例题:月度开销
分形(找规律)
例题:循环比赛日程表
8位选手的循环比赛表可以由四名选手的循环比赛表根据对称性“生成出来”
分治思想:不断的把一个规模为n的问题分成4个规模为n/2的子问题
//左上角(a1,b1),右下角(a2,b2),[begin, end]的数字组成
void makelist(int a1, int b1, int a2, int b2, int begin, int end)
{
//问题的边界
if (begin == end){
a[a1][b1] = begin;
return ;
}
//求解子问题
//分成四个区域,左上角区域的右下角坐标是(a3, b3)
int a3 = (a1 + a2) / 2, b3 = (b1 + b2) / 2;
int mid = (begin + end) / 2;
makelist(a1, b1, a3, b3, begin, mid); //左上角区域
makelist(a1, b3+1, a3, b2, mid+1, end); //右上角区域
makelist(a3+1, b1, a2, b3, mid+1, end); //左下角区域
makelist(a3+1, b3+1, a2, b2, begin, mid); //右下角区域
}
//函数调用
makelist(1, 1, n, n, 1, n); //左上角(1,1),右下角(n,n),由1~n数字组成
// 这道题目也可以用从小大到递推出来整个地图
例题:Fractal Streets
//分形,著名的通过一定规律无限包含自身的“分形”图。
进阶的有,三分,0/1分数规划
// 提高组再学
《一本通》题目
【例7.4】 循环比赛日程表
//分形
【例7.5】 取余运算(mod)
//快速幂
【例7.6】黑白棋子的移动
//这道题目研究很长时间。找规律,写代码都花费了较长时间
【例7.7】光荣的梦想
//逆序对
2011
//快速幂
//2011的幂次有一个规律,2011^1 和2011^501 是相同的,每500一个循环
输出前k大的数
//排序
区间合并
//读懂题意后,排序,从左往右贪心
求排列的逆序数
//逆序对
一元三次方程求解
//check函数是f(x) * f(mid) < 0, 而不是f(mid) == 0之类的
统计数字
//不能用STL,排序一下,双指针就行了
查找最接近的元素
//数列上进行二分查找,找边界
二分法求函数的零点
//二分答案(浮点数二分模板,写check函数)
网线主管
//二分答案(整数二分模板,写check函数)
月度开销
//二分答案,根据题意写check函数,这块经常写不好。
//理解一下,数轴上表示单调性的图,右侧区间都是答案。(如果月度开销小了话,就会需要超过M个预算周期,就不满足条件了)
和为给定数
//数轴上二分查找就可以了,唯独这道题目不能用STL
不重复地输出数
//直接用set<int>处理了
膨胀的木棍
花费了很长时间的题目,不太了解几何概念。
//公式,弧度,角度,PII,acos(-1)=PII,弧长= 半径*弧度
//l = degree * r 圆心角弧度数 * 半径
//r * sin(degree) * 2 = d; 弦长
//so, 2 * sin(degree) * l = d * degree
double L2 = (1 + N * C) * L; //弧长
double l = 0.0, r = acos(-1.0), ans = 0.0; //0, 180度
while (r - l > eps){
double mid = (l + r) / 2;
if (L * mid + eps < 2 * sin(mid/2) * L2) l = mid, ans = mid;
else r = mid;
}
double banjing = L2 / ans;
double x = banjing - banjing * cos(ans / 2);
printf("%.3lf\n", x);
河中跳房子
//noip2015tg
//二分答案,这道题目做的时候,还是输在了写check函数上
//1.怎么去表达搬走石头的操作 2.求的是最长的最短距离,也就是mid左侧都是答案,注意边界的写法
//我做的时候,最初的时候,理解成了mid的右侧是答案,造成了很多迷惑。后来发现mid左侧是答案后,就迎刃而解了