
杭电acm入门
Tanpinsary第一讲 数学基础
before: 多组输入的处理
输入处理
-
类型一:输入不说明有多少input block,默认以**
EOF
**为读入结束
例1:A+B Problem
1 |
|
scanf函数的返回值为成功输入的变量个数,无读入返回**EOF
(为-1**,而非0),因而我们还可以这样:
1 |
|
若使用cpp,更加方便
1 | while(cin >> a >> b) { |
-
类型二:输入开始告知有N个Input Block
先读入N,在借助while或者for循环实现
-
类型三:不说明有多少Input Block,但是以特殊输入为结束标志
处理方式类似类型一——先读
仍然以A+B Problem为例,当a与b均不为0时结束
1 |
|
更好地:(可读性更好)
1 | while(scanf("%d %d", &a, &b)) { |
-
类型四:前三种的组合问题
-
类型五:字符串的输入问题
例:HDOJ_1048
若c:
1 | char buf[20]; |
若cpp:
1 | //如果用 string buf; 来储存 |
输出处理
-
类型一:每个Input Block对应一个Output Block,Output Block之间没有空行
-
类型二:每个Input Block对应一个Output Block,Output Block之后有一个空行
-
类型三:每个Input Block对应一个Output Block,Output Block之间有一个空行
小差别注意即可
注意:
多组数据,是否需要一次性全部读取,再一组一组处理?
不需要
提交到OJ的程序从文件中读取,然后生成临时文件存储输出,与标准Output进行逐字符对比处理
导引:整数求和()
-
等差数列的公式求和的风险?
,N过大时,会爆`int`(32位)解决方案:
- 64位整数(long long, %lld)
例一:求最小公倍数
-
暴力枚举?TLE
-
数学方法:
注意点同导引,如何防止过程中爆
int
?先÷后x -
求GCD?辗转相除法(欧几里得算法)
- 级的算法
- 原理
- 终止条件:有一方为0,另一方为gcd
1
2
3
4
5
6
7
8
9int gcd(int a, int b) {
int temp;
while(b != 0) {
temp = a % b;
a = b;
b = temp;
}
return a;
} 为什么不需要在意大小?第一轮处理会交换
1
2
3//递归写法
int gcd(int a, int b)
return (b != 0) ? gcd(b, a % b) : a;
例二:计算N个N相乘的个位数——规律
- ()
- 暴力(TLE)
- 每次处理后模10?——解决了数据溢出,但是循环次数仍然太多
- 找到规律循环节,后打表
为什么去找规律?
- 数据规模太大
- 暴力计算不可行
例三:特别的fibonacci数列——规律与循环节
给定一个n(n<1000000),请判断F(n)能否被3整除,分别输出yes和no
-
递归计算每一项,模3 数据规模太大
-
同余定理:
-
规律:发现循环节
G(1) G(2) G(3) G(4) G(5) G(6) G(7) G(8) G(9) G(10) G(11) 1 2 0 2 2 1 0 1 1 2 0 循环节为8
-
为什么一定会有循环节?
——抽屉原理
由于%3结果一定是0 1 2,且前两项导出后一项,考虑所有相邻两项的组合,共有9种组合,且0 0情况可排除,即8种,则根据抽屉原理(容斥原理),则循环节在大致9位内会出现,一旦出现了重复的相邻两项,则一定出现了循环节
例四:求A^B最后三位数表示的整——快速幂
-
找规律不可行 样本太大了
-
快速幂(二分加速)
-
递归实现
1
2
3
4
5
6
7
8
9
10
11int power(int a, int n) {
int ans;
if(n == 0)
ans = 1;
else {
ans = power(a * a, n / 2);
if(n % 2 == 1)
ans *= a;
}
return ans;
} -
非递归实现
1
2
3
4
5
6
7
8
9
10int power(int a, int n) {
int ans = 1;
while(n) {
if(n % 2)
ans *= a;
a = a * a;
n /= 2;
}
return ans;
} -
特别注意:小心爆**
int
**!! -
快速幂基本都伴随着取模运算
-
第二讲 贪心算法
- 总是做出当前来看的最优解
- 分解子问题,对子问题按照同样的策略进行操作
例一:田忌赛马
已知田忌与国王所有马的能力值,赢的人获得200元,如何找到最优解?
-
贪心策略
用自己的马挑战对方比自己弱的最小的马
-
确定思路
- 排序
- 比较未标记的最好的马,自己的能超过对方的马吗
- 如果可以,则赛马
- 如果不可以,则用自己最差的马和对方最好的马赛马
- 赛马做标记
- 重复策略直至所有的马比完
例二:事件序列问题
-
贪心策略
拿结束时间排序,尽量留出更多的时间
-
确定思路
-
提出命题:至少存在一个最长的事件序列,包含最早结束的事件
-
证明:反证法
逆命题:所有的最长事件序列都不包含最早结束的事件
在该假设下任取一个最长的事件序列,将第一个事件与最早结束的事件替换,则构造了一个相同长度的最长的时间序列且含有最早结束的事件,逆命题不成立,原命题得证
-
-
按照结束时间排序
-
找到最早结束事件作为第一个事件
此时问题分解为
n-1
阶的子问题,注意额外分析是否会冲突 -
**注意!!**选出的结果方案不唯一,而方案的序列长度一定唯一
-
例三:搬桌子
丁爸信奥培训中心最近在富丽科技大厦租了一层楼,这层楼的形状如下:
由图可见,这层楼中间是走廊,两侧各有200个房间,编号如上图。
最近,丁爸信奥培训中心做了内部机构的调整,需要把一些桌子从一个房间搬到另外的房间。因为走廊很窄,但是桌子很大,所以同一段走廊每次只能通过一个桌子。 假设不论远近,每趟搬桌子都需要10分钟。同时,当你从房间i搬桌子到房间j的过程中,房间i到房间j之间的走廊都被占用,也就是说,在每个10分钟内,不能有多个任务共享同一段走廊。
现在,丁爸想知道:要完成所有的搬运任务,最少需要多少时间?
-
问题分析:
-
为什么不能找多次最长事件序列?
最长事件序列只能保证单一最优解,全局并非最优解
-
-
贪心策略
-
最优策略:记录路径,找到重叠最多地方的重叠次数,即为所需最多的次数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using namespace std;
int p[201];
int main() {
int T, ans;
cin >> T;
while(T --) {
int n;
cin >> n;
int s,t;
memset(p, 0, sizeof(p));
ans = 0;
for(int i = 0; i < n; i ++) {
cin >> s >> t;
s = (s + 1) / 2;
t = (t + 1) / 2;
if(s > t)
swap(s, t);
for(int j = s; j <= t; j++)
p[j]++;
}
for(int i = 1; i <= 200; i ++)
if(p[i] > ans)
ans = p[i];
cout << ans * 10 << endl;
}
return 0;
} -
较差策略:按照开始位置排序,从第一个需要搬的桌子开始i,一次性找到所有线路不冲突的桌子,不断分解子问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
using namespace std;
struct M{
int s;
int t;
};
bool comp(const M &m1, const M &m2){
return m1.s < m2.s; //按照开始的房间号排序
}
int main(){
int T, s, t;
scanf("%d", &T);
while(T--) {
int n;
vector<M> v;
scanf("%d", &n);
for(int i = 0; i < n; ++i) {
M m;
scanf("%d%d", &s, &t);
if(s > t)//有可能从号码大的房间往回搬,若是,则交换s与t
swap(s, t);
m.s = s;
m.t = t;
v.push_back(m);
}
sort(v.begin(), v.end(), comp);
int ans = 0;
for(vector<M>::iterator it = v.begin(); it != v.end();){ //从房间号最小的开始查找
++ans;
int t = (*it).t;
for(vector<M>::iterator subit = it; subit != v.end();) { //找到与要搬的线路不冲突的桌子
if((*subit).s > t && !((*subit).s - t == 1 && (*subit).s % 2 == 0)) { //注意对面房间的线路也是冲突的,比如5与6
t = (*subit).t;
subit = v.erase(subit); //该桌子已搬完,删除
} else
++subit; //否则继续查找下一张桌子
}
it = v.erase(it); //删除
}
printf("%d\n", ans * 10);
}
return 0;
}
-
例四:删数问题
有一个长度为n(n <= 240)的正整数,从中取出k(k < n)个数,使剩余的数保持原来的次序不变,求这个正整数经过删数之后最小是多少。
-
子问题分解:每一次删除数字都使得剩下的正整数保持最小
-
贪心策略:
删除从左向右第一个逆序列的第一个数字
或 删除从左到右递增序列结束处的数字
1 |
|
例五:青蛙的邻居
未名湖附近共有N个大小湖泊L1, L2, …, Ln(其中包括未名湖),每个湖泊Li里住着一只青蛙Fi(1 ≤ i ≤ N)。如果湖泊Li和Lj之间有水路相连,则青蛙Fi和Fj互称为邻居。现在已知每只青蛙的邻居数目x1, x2, …, xn,请你给出每两个湖泊之间的相连关系。
Input
第一行是测试数据的组数T(0 ≤ T ≤ 20)。每组数据包括两行,第一行是整数N(2 < N < 10),第二行是N个整数,x1, x2,…, xn(0 ≤ xi ≤ N)。
Output
对输入的每组测试数据,如果不存在可能的相连关系,输出"NO"。否则输出"YES"
-
离散数学:可图形判定
-
度序列
若把一个图G所有顶点的度数台城一个序列S,则称S为图G的度序列
-
度序列是可图的
一个非负整数组成的有限序列如果是某个无向图的度序列,则称该序列是可图的
-
Havel-Hakimi定理
由非负整数组成的有限非递增序列,,当且仅当也是可图的
-
-
贪心策略:
通过havel-hakimi定理,对序列由大到小排序,然后对操作,排序,对操作,排序……直到序列全部变成0。如果出现负数,则该度序列是不可图的
第三讲 并查集
导引问题
在某个城市里住着n个人,现在存在关于n个人的m
条信息,假设所有认识的人为一个单位,最多有多少个单位
什么是并查集
-
Disjoint Set,即“不相交的集合”
N个元素的N个对象划分为不相交集合,每个集合中选定某个元素代表所在集合
-
常见两种操作
- 合并两个集合
- 查找某个元素属于哪个集合
实现方法一
-
用编号最小的元素标记所在集合
-
定义一个数组,其中表示元素所在集合
Set(i) 1 2 1 4 2 6 1 6 2 2 i 1 2 3 4 5 6 7 8 9 10 不相交集合:
-
两个功能
-
查
1
2int find(x)
return Set[x]; -
并
1
2
3
4
5
6
7
8void Merge(a, b) {
int i = min(a, b);
int j = max(a, b);
for(int k = 1; k <= N; k++) {
if(Set[k] = j)
Set[k] = i;
}
}
-
实现方法二——树
-
利用树实现
-
定义数组
- 若,说明表示该集合,是该集合的对应树的根
- 若,说明是的父节点
-
两个功能
-
查 最坏
1
2
3
4
5
6int find(x) {
int r = x;
while(Set[r] != r)
r = Set[r];
return r;
} -
并
1
2void merge(a, b)
Set[a] = b; -
避免出现最坏情况
-
方法:将深度小的树合并到深度大的树
-
实现:假设两棵树的深度,,则合并后的树的高度为
-
效果:任何顺序的合并操作后,包含k个节点的树的最大高度不超过
-
-
优化后
1
2
3
4
5
6
7
8
9void merge(a, b) {
if(height[a] = height[b]) {
height[a]++;
Set[b] = a;
} else if(height(a) < height(b))
Set[a] = b;
else
Set[b] = a;
}此时,查的效率已经变为最坏
-
实现方法三——带路径压缩的查找操作
1 | int find(x) { |
例一
经典案例:最小生成树
-
Prim算法
-
Kruskal算法
-
理论基础——MST性质
命题:至少存在一颗最小生成树含有权值最小的边
证明:类似第二讲例二的证明
- 算法步骤:(贪心算法)
- 选中最小的边
- 找到下一个最小的边,若两个顶点不连通,则选中,重复
- 直到选中条边
-
例二
第四讲 递推求解
-
递推关系式(状态转移方程)
将用与增减量表示 可能与前面的多项有关系
例一——状态产生分析
一个平面上有条直线,这些直线中每一条在圆内同其他直线相交,假设没有三条直线相交,试问能分成多少区域
- 分析:假设此时有n-1条直线分割圆,令第n条直线把上述n-1条直线分割产生n-1个端点,n条线段,即可分割产生n个新区域
初始状态:
状态转移方程:
例二
平面上有条折线,这些折线最多能将平面分割为多少块?
-
分析:假设此时有n-1条直线分割平面,第n条折线可以做到与前n-1条折线都产生四个交点
状态转移方程:
例三——状态向前分析
在的长方形方格中,用n个的骨牌铺满方格,有多少种铺设的方案
-
例一与例二都是从状态产生的角度去思考状态转移方程
-
然而 例三给出了另一种思考方式
从第个状态向前分析成因
第n列格子分为横竖两种状态
若为竖,则前有F(n-1)种状态
若为横,则n列和n-1列为双横,前有F(n-2)种状态,故:
状态转移方程:
-
核心:大问题分类讨论分别解决
例四
有的长方形,用,,的骨牌铺满方格,求第n个多少种铺法
-
分析同例三
状态转移方程:
总结
- 首先:确认能否容易得到初始状态的解
- 假设:规模不大于的状态已经得到了解决
- 最后:重点分析当规模扩大到时,如何枚举当前的情况,然后用子问题生成
例五——合法性角度分类
所有学生站成一排,规定女孩不能单独站,即要么队列中没有女生,要么不止一个女生并排站
比如,时,有下面合法队列:
FFFF、FFFM、MFFF、FFMM、MFFM、MMFF、MMMM
给定人数求可能的合法队列数
- 两种做法,一种分男女考虑合法与非法状况
- 另一种设计子函数分别表示男女来生成
例六
有排成一行的个方格,用R,P,G三色进行涂色,每格涂一种颜色,要求航信放个不能同色,首位两格也不能同色
- 依旧是合法性分类
状态转移分析:
- 前n-1格子序列合法的情况: 此时第n个格子不能与合法的第一个和第n-1个颜色相同,只能有一种选择
- 前n-1格子序列非法的情况: 若想在第n个格子添加后生成合法的格子序列,该非法类型只能是第一个格子与第n-1个格子同色 这一部分的非法序列来源于合法的n-2序列添加一个首色格子,为 此时的最后一个格子有两种颜色可以选择
故:
例七
个数字按照顺时针方向围成一个圆,条线段把所有数字两两相连,要求不能有交叉求:有多少合法的连线方案
-
卡特兰数列(Catalan)
1、2、5、14、642、132,……
-
高精度组合问题
第五讲 动态规划
典例1
![[Pasted image 20250225215005.png]] 有如上数塔,从顶部出发,在每一个节点都可以选择向左走或者向右走,一直走到底层,要求找到一条路径善意的路径上数值和最大
- 首先考虑暴力求解,如何遍历所有路径,显然有条路径,不予考虑
- 分解子问题: 从顶点开始考虑左右两侧的最优解(类似二叉树考虑左右子树的最好情况) 时间复杂度:
- 结论:自顶向下的分析,自底向上的计算
例一:免费馅饼
数轴上可能在某一时刻在某一位置掉落免费馅饼,0秒时人在5m处,每秒钟人只能移动一米,给出馅饼掉落的位置与时刻,给出获得馅饼最多的最优解
- 显然问题仍然在于分解时间寻找最优解,问题是如何分解?
- 注意到人的操作只有向左或向右或停止不动,那么将一维的数轴按照时间作为第二个轴进行绘图,发现其实就是数塔问题
典例二:最长有序子序列
I | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|---|
Num[I] | 1 | 4 | 7 | 2 | 5 | 8 | 3 | 6 | 9 |
- 如何分解子问题?
- 考虑到序列的寻找是从无至有的,也就是可以将问题规模从小到大考虑
I | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|---|
Num[I] | 1 | 4 | 7 | 2 | 5 | 8 | 3 | 6 | 9 |
F[I] | 1 | 2 | 3 | 2 | 3 | 4 | 3 | 4 | 5 |
核心思想:利用已经解决的子问题作为已知条件进行演算后续问题(即:站在巨人的肩膀上看问题)
例二:胖老鼠问题
给出若干个老鼠的体重和速度,找到一个最长的序列,使得体重越来越大,速度越来越低
- 按照体重递增排序,然后按照速度降序的原则寻找最长递减有序子序列
例三:最少拦截系统
给出n个飞弹,并且给出这n个飞弹的高度,有拦截系统进行拦截,且拦截系统拦截的多个导弹的高度只能递减
1:一套拦截系统最多可以拦截几个导弹? 仍然是最长不递增有序子序列问题 2:最少需要几套拦截系统才能拦截所有导弹?
-
尝试模拟?单次都做到拦截最多——贪心算法
-
问题的本质:最少分成几个”最长不递增有序子序列问题“
- Dilworth定理: 对于一个偏序集,最少链划分数等于最长反链长度
- 结论:本体的本质为 求最长上升子序列的长度
例四:搬宿舍
搬宿舍,只能左手一个右手一个,搬东西消耗的能量为两者质量差的平方成正比。 已知有n个物品,需要搬k次,分别给出所有的物品重量,求消耗的最少能量
- 第一感觉:两手的东西质量尽可能接近
- 粗略思考验证:搬运一次必须要搬运质量相邻的两者
- 预处理:排序
- 问题的分解:搬运k次的最优解建立前k-1次的最优解情况,考虑递推,考虑dp
- 设为在前n个物品中搬运k次,则可以分解问题
- 如果拓展到呢? 其实还是在分解问题,搬运第k次有两种,一种是这k次搬走了第n个物品,一种是没有,那么状态转移方程按照上述的思路即可解决:
- 注意问题的初始状态:问题规模在以下的都可以作为初始状态,的解显然为
小结:
- 基本思想:如果各个子问题不是独立的(即:重复的),不同的子问题的个数只是 多项式量级 (即:有限的),如果我们能用 数组 来保存已解决的子问题的答案,在需要时直接获取已求得的答案,这样就可以避免大量的重复计算
- 特点:
- 最优子结构 大问题的最优解一定包含子问题的最优解
- 重叠子问题 子问题是重叠的,即大规模问题的解决牵扯到小规模问题
- 无后效性 完成小规模问题后再完成大规模问题的时候,大规模问题的解决不会影响小规模的问题
第六讲 背包问题
导引问题
现有一张餐券面值10元,有菜肴N种,餐券一次性使用不可找零,问如何选择菜肴使得餐券使用最不浪费
什么是背包问题
- 给定容量为的背包和若干物品,在一定的限制条件下问最多能放进多少价值的物品
- 最经典的DP问题
- 背包问题中状态的理解(状态与状态转移)
背包问题分类
- 01背包(非常重要!!!)
- 完全背包
- 多重背包
- 二维费用背包
- 混合三种背包
- 分组背包
- 有依赖的背包
01背包
- 特点:每种物品仅有一个
- 有个物品和一个容量为的背包,第件物品的费用是,价值是,求解放入哪些物品使得价值总和最大
- 问题特点:每种物品仅有一件,仅能选择放or不放
- 子问题分解? —— 基于状态
- 无须→有序? 从 到 依次考虑各个物品
- 状态定义:表示考虑前件物品后放入一个容量为的背包可以获得的最大价值
例一
张三喜欢收集骨头,每个骨头有自己的价值和体积,求问背包可以装下最多价值的物品 例如:体积为10,有5块骨头,价值分别为1,2,3,4,5,体积分别为5,4,3,2,1
- 仍然采取 第五讲例四 的思路
- 从前0个物品依次考虑前 个物品,按照这个顺序从背包体积为0到10依次进行分析
- 没装第个物品——价值为(继承上一行的价值)
- 装第个物品——此时的背包容量相比上一行能否放得下?如果放得下,放入并计算价值
- 以上两个情况取
max
记录这个格子内 - 得到了下面的表格(程序的执行过程即为从第一行到最后一行一次从左向右遍历
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | |
0 | 0 | 0 | 0 | 2 | 2 | 2 | 2 | 2 | 3 | 3 | |
0 | 0 | 0 | 3 | 3 | 3 | 3 | 5 | 5 | 5 | 5 | |
0 | 0 | 4 | 4 | 4 | 7 | 7 | 7 | 7 | 9 | 9 | |
0 | 5 | 5 | 9 | 9 | 9 | 12 | 12 | 12 | 12 | 14 |
- 二维背包最能反映01背包的本质——考虑前1个,前2个,前3个……前i个,当背包体积最大时,得到的结果就是最优解
- 状态转移方程:
- 时间复杂度与空间复杂度都为
- 空间复杂度的优化:使用一维数组DP[j],从后向前遍历
1
2
3for i = 1 to n
for j = V to v[i]
dp[j] = max(dp[j], dp[j-v[i]] + w[i])
完全背包
- 特点:一种物品可以取无数个
- 可否转化为01背包?
多重背包
- 二进制优化
1 | //预处理 |
二维费用背包
第七讲 BFS入门
- 预备知识 队列 stl queue
层次遍历
- 二叉树
- 图
例
- BFS标准套路
第八讲 DFS入门
导引 递归
- 特征:
- 先写出口(不需递归的特殊情况)
- 再写普遍情况(递归解决)
例一 全排列
- 递归特征:
- 如果第一个数确定,则剩余问题就是其余 个数字的全排列
- 如果前 个数确定,则剩余问题就是其余 个数字的全排列
- 枚举当前的每一种情况
- 在每一种可能中,递归调用dfs(step + 1)
1 |
|
- 图解(图源自dfs深度搜索全排列图解详细步骤(图解版) - 白色飞碟 - 博客园 大佬太牛了)
例二 迷宫搜索
第八讲 二分匹配
- 二分图