文章目录
进制转换
8➡10:
12
3
o
c
t
=
3
×
8
0
+
2
×
8
1
+
1
×
8
2
=
8
3
d
e
c
123_{oct} = 3 \times8^0+2\times8^1+1\times8^2 = 83_{dec}
123oct=3×80+2×81+1×82=83dec
10➡8:
8
3
d
e
c
÷
8
=
10
⋅
⋅
⋅
⋅
⋅
⋅
3
1
0
d
e
c
÷
8
=
1
⋅
⋅
⋅
⋅
⋅
⋅
2
1
d
e
c
÷
8
=
0
⋅
⋅
⋅
⋅
⋅
⋅
1
↑
\left.\ \begin{aligned} 83_{dec} \div 8 = 10 ······3\\ 10_{dec}\div 8 = 1······2\\ 1_{dec} \div8 = 0······1\\ \end{aligned} \right\uparrow
83dec÷8=10⋅⋅⋅⋅⋅⋅310dec÷8=1⋅⋅⋅⋅⋅⋅21dec÷8=0⋅⋅⋅⋅⋅⋅1⏐⏐⏐⏐⏐⏐↑
其他的都是类似的。从低进制到高进制就按照第一种方式,否则按照第二种方式,二者实际上是一个逆过程。其他的可以先转换成十进制,然后再转换成目标进制。
最大公约数与最小公倍数
最大公约数可以用于分数运算的时候的化简。
最大公约数
最大公约数可以利用欧几里得算法(辗转相除法)进行求解。
设a、b均为正整数,则gcd(a,b) = gcd(b, a%b)
有了递推式之后,还需要一个递归边界,因为0与任意数字a的最大公约数为a,所以可以利用这个作为递归的出口。
在这里需要a>b所以应该在先判断a和b的大小。
int gcd1(int a, int b){
if(a < b){
int temp = a;
a = b;
b = temp;
}
if(b == 0) return a;
else return gcd(b, a % b);
}
//等价形式
int gcd2(int a, int b){
if(a < b){
int temp = a;
a = b;
b = temp;
}
return !b ? a : gcd(b, a % b);
}
//辗转相除法
int gcd3(int a, int b){
if(a < b){
int temp = a;
a = b;
b = temp;
}
while(b != 0){
int temp = a % b;
a = b;
b = temp;
}
return a;
}
素数
判断素数
素数的定义是除了1和本身之外不能被其他整数整除的一类数。因为假设一个数字k可以整除数字n,那么有 n k × k = n \frac nk \times k=n kn×k=n所以这里可以得到, n k \frac nk kn也是可以整除n的。这也就是说这两个数一定是一个大于等于sqrt(n),一个小于sqrt(n)的,所以只需要遍历2~sqrt(n)(范围之内的所有整数,检查是否存在能够整除目标数字的数即可)(反证法,假设该数能够整除目标数字,而能够整除目标数字的数是成对出现的)
bool isPrime(int n){
if(n <= 1)return false;
else{//这个else语句是可以不要的
int sqr = sqrt(1.0 * n);//由于sqrt函数需要浮点型,所以这里乘以1.0
for(int i = 2; i <= sqr; ++i){
if(n % i == 0)return false;
}
return true;
}
}
素数表的获取
由于判断一个数字是否属于素数的时间复杂度为O( n \sqrt{n} n),所以想要获取n个素数的时间复杂度为O(n* n \sqrt{n} n )
const int maxn = 101;
int prime[maxn], pNum = 0;
bool p[maxn] = {0};
void Find_Prime(){
for(int i = 1; i < maxn; ++i){
if(isPrime(i) == true){
prime[pNum++] = i;
p[i] = true;
}
}
}
还有一种算法是欧拉筛选法,其时间复杂度为O(nlong long n)
质因子分解
质因子分解就是将一个非质数分解成素数的乘积,素数在乘积中可以重复出现。
对一个正整数n 来说,如果它存在[2, n]范围内的质因子,要么这些质因子全部小于等于sqrt(n),要么只存在一个大于sqrt(n)的质因子,而其余质因子全部小于等于sqrt(n)
算法思想:求出素数表,然后用输入的参数x与该素数取余数,直到无法被整除,换下一个素数,直到x变成0。之后按照格式进行输出。
//自己写的,水平有限,后续会对此进行优化
//判断是否为质数
bool isPrime(int x) {
if (x < 2)return false;
double sq = sqrt(x * 1.0);
int num = 2;
while (num <= sq) {
if (x % num == 0)return false;
num++;
}
return true;
}
//质数的结构体,包含质数数字本身,以及在分解式中出现的次数
struct prime{
int data = 0;
int num = 0;
};
//质因子分解
void prime_factorization(int x) {
prime prime_list[200];
int index = 0;
int copy = x;
//考虑到输入参数本身就是质数的情况,边界设置为i<=x
for (int i = 2; i <= x; ++i) {
if (isPrime(i))prime_list[index++].data = i;
}
for (int i = 0; i < 10; ++i) {
int temp = prime_list[i].data;
while (x % temp == 0 && x != 0) {
prime_list[i].num++;
x /= temp;
}
}
//输出
printf("%d=", copy);
bool flag = true;//标记是否为第一个数字
for (int i = 0; i <= index; ++i) {
for (int j = 0; j < prime_list[i].num; ++j) {
if (flag != true)printf("*");
//printf("*");
printf("%d", prime_list[i].data);
flag = false;
}
}
//getchar();
}
大数运算
大数乘法
将两个数字按照位从小到大从低位到高位依次加入到数组中。将其中的一个乘数从按位从小到大乘以另外一个数,加到数组中。每一位只加上计算结果的个位数,进位加到下一位中。最后的结果数组中可能还会有某一位超过十的数字的位,这时候就取余,调整成各位就可以了。
//初级版本,还有在后序进行改进的
void stimuMulti(int a[], int an, int b[],int bn) {
//初始化结果数组
int result[100];
fill(result, result + 100, 0);
//计算各位的值
for (int i = 1; i <= bn; ++i) {
int carry = 0;//进位
int index_result = i;//错位相加(开始的位置错一位)
//关键部分
for (int j = 1; j <= an; ++j) {
result[index_result++] += (b[i] * a[j]) % 10;
carry = (a[j] * b[i]) / 10;
result[index_result] += carry;
}
//将最后一位的下一位作为结束标志
if (i == bn)result[++index_result] = -1;
}
//调整,将大于十的位调整为小于十
int i = 1;
while (result[i++] != -1) {
if (result[i] >= 10) {
int carry = result[i] / 10;
result[i] %= 10;
//当最后一位需要进位的时候
if (result[i + 1] == -1) {
result[i + 2] = -1;
result[i + 1]++;
}
result[i + 1] += carry;
}
}
//输出
cout << endl;
bool flag = false;
for (int j = i - 2; j >= 1; --j) {
//将最高位前面的0全部忽略掉
if (result[j] == 0 && flag == false) {
continue;
}
flag = true;
printf("%d", result[j]);
}
if (flag == false)printf("0");
几何问题
由三点的坐标求所构成的三角形的面积
有几个比较常用的计算面积公式:
S
Δ
A
B
C
=
1
2
∣
a
⃗
∣
∣
b
⃗
∣
s
i
n
<
a
⃗
,
b
⃗
>
S_{\Delta ABC} = \frac 12 \vert \vec a\vert \vert \vec b\vert sin<\vec a,\vec b >
SΔABC=21∣a∣∣b∣sin<a,b>
海伦公式:
S
Δ
A
B
C
=
p
(
p
−
a
)
(
p
−
b
)
(
p
−
c
)
,
其
中
p
=
a
+
b
+
c
2
S_{\Delta ABC} = \sqrt {p(p-a)(p-b)(p-c)},其中p = \frac {a+b+c}{2}
SΔABC=p(p−a)(p−b)(p−c),其中p=2a+b+c
//利用了第一个求面积的公式
struct point{
double x,y;
};
//求两点形成的向量
point decPoint(point p1, point p2){
point res;
res.x = p1.x - p2.x;
res.y = p1.y - p2.y;
return res;
}
//求两向量的叉乘
double mutiPoint(point p1, point p2){
return (p1.x*p2.y - p2.x*p1.y);
}
//求面积
double areaOfThreePoint(point a, point b, point c){
return fabs(multipoint(decPoint(B, A), decPonint(C, A))/2.0);
}
判断点是否在三角形内
针对这个问题有几个虽然麻烦但是十分直观的做法。1.计算该点和三角形三点组成的面积之和,若于三角形面积相等,则该点在三角形内。2.计算该点与三角形所有任意两点组成的夹角之和,判断是否等于360°。
还有一种是利用叉乘。想要该点在三角形内,只需要该点在三条边的下面/上面。如果该点在三角形内部,那么该点与三角形三点形成的向量和三角形三条边形成的向量的叉乘的方向是一致的。叉乘的计算方式如下:
设
a
(
x
1
,
y
1
)
,
b
(
x
2
,
y
2
)
,
则
a
⃗
×
b
⃗
=
(
x
1
∗
y
2
−
x
1
∗
y
1
)
j
⃗
,
a
⃗
×
b
⃗
=
∣
a
∣
∣
b
∣
∗
s
i
n
(
θ
)
设a(x_1,y_1),b(x_2,y_2),则\vec a×\vec b = (x_1*y_2-x_1*y_1)\vec j, \vec a×\vec b = \vert a\vert \vert b\vert*sin(\theta)
设a(x1,y1),b(x2,y2),则a×b=(x1∗y2−x1∗y1)j,a×b=∣a∣∣b∣∗sin(θ)
![向量叉乘图示](https://upload.wikimedia.iwiki.eu.org/wikipedia/commons/thumb/b/b0/Cross_product_vector.svg/800px-Cross_product_vector.svg.png)
所以可以借用两个向量的叉乘的正负情况,判断二者是否在同一方向上。
算法思想:创建一个方向函数,将三点的坐标作为参数,计算出各边的向量,然后计算出叉乘,将叉乘的结果的正负作为返回值。判断所有的叉乘结果的符号是否都相等。
//转载自牛客网https://www.nowcoder.com/questionTerminal/f9c4290baed0406cbbe2c23dd687732c
#include <bits/stdc++.h>
using namespace std;
int direction(double x1, double y1, double x2, double y2, double x3, double y3){
/*
求向量V和点(x3, y3)之间的位置关系,(x1, y1)是V的起点,(x2, y2)是V的终点
*/
//得到向量V
x2 -= x1;
y2 -= y1;
//V的起点(x1, y1)与目标点(x3, y3)构成新的向量U
x3 -= x1;
y3 -= y1;
double res = x3 * y2 - x2 * y3;//求两向量U和V的外积(叉积)
if(res < 0) return -1;//<0在逆时针方向
if(res == 0) return 0;//=0共线
if(res > 0) return 1;//>0说明点在向量顺时针方向
}
int main(){
double x1, y1;
double x2, y2;
double x3, y3;
cin >> x1 >> y1;
cin >> x2 >> y2;
cin >> x3 >> y3;
double x4, y4;
cin >> x4 >> y4;
//(x1, y1)与(x2, y2)构成向量V1
//(x2, y2)与(x3, y3)构成向量V2
//(x3, y3)与(x1, y1)构成向量V3
int res1 = direction(x1, y1, x2, y2, x4, y4);
int res2 = direction(x2, y2, x3, y4, x4, y4);
int res3 = direction(x3, y3, x1, y1, x4, y4);
if(abs(res1 + res2 + res3) == 3){//全-1或全1,表示点(x4, y4)同时在三个向量的同一侧
cout << "Yes";
}else {
cout << "No";
}
return 0;
}
集合问题
子集问题
原文:https://mp.weixin.qq.com/s/qT6WgR6Qwn7ayZkI3AineA
数学归纳法
对于
{
1
,
2
,
3
}
\{1,2,3\}
{1,2,3}的子集,通过观察发现,原数组的子集跟它子集的子集是有关联的。注意到:
S
u
b
s
e
t
{
1
,
2
,
3
}
−
S
u
b
s
e
t
{
1
,
2
}
=
{
3
}
,
{
1
,
3
}
,
{
2
,
3
}
,
{
1
,
2
,
3
}
Subset_{\{1,2,3\}} -Subset_{\{1,2\}} = \{3\},\{1,3\},\{2,3\},\{1,2,3\}
Subset{1,2,3}−Subset{1,2}={3},{1,3},{2,3},{1,2,3}
而这个子集又是可以从
{
1
,
2
}
\{1,2\}
{1,2}的子集中,每一个子集加上一个元素3之后的结果。同样,
{
1
,
2
}
\{1,2\}
{1,2}的子集又可以通过
{
1
}
\{1\}
{1}的子集加上元素2之后得来。
所以,
S
u
b
s
e
t
{
1
,
2
,
3
}
=
S
u
b
s
e
t
{
1
,
2
}
+
{
S
u
b
s
e
t
{
1
,
2
}
a
p
p
e
n
d
{
3
}
}
S
u
b
s
e
t
{
1
,
2
}
=
S
u
b
s
e
t
{
1
}
+
{
S
u
b
s
e
t
{
1
}
a
p
p
e
n
d
{
2
}
}
S
u
b
s
e
t
{
1
}
=
S
u
b
s
e
t
{
}
+
{
S
u
b
s
e
t
{
}
a
p
p
e
n
d
{
1
}
}
Subset_{\{1,2,3\}} = Subset_{\{1,2\}} + \{Subset_{\{1,2\}} append \{3\}\}\\ Subset_{\{1,2\}} = Subset_{\{1\}} + \{Subset_{\{1\}} append \{2\}\}\\ Subset_{\{1\}} = Subset_{\{\}} + \{Subset_{\{\}} append \{1\}\}
Subset{1,2,3}=Subset{1,2}+{Subset{1,2}append{3}}Subset{1,2}=Subset{1}+{Subset{1}append{2}}Subset{1}=Subset{}+{Subset{}append{1}}
所以可以将空数组作为基元素。
vector<vector<int>> subsets(vector<int>& nums) {
// base case,返回一个空集
if (nums.empty()) return {{}};
// 把最后一个元素拿出来
int n = nums.back();
nums.pop_back();
// 先递归算出前面元素的所有子集
vector<vector<int>> res = subsets(nums);
int size = res.size();
for (int i = 0; i < size; i++) {
// 然后在之前的结果之上追加
res.push_back(res[i]);//这里是将原先的数组赋值了一份过来然后再往结尾的地方加上最后一个元素
res.back().push_back(n);
}
return res;
}
但是这种写法的时间复杂度和空间复杂度很大[O( n × 2 n n\times 2^n n×2n)],下面用的是回溯法去求解。
回溯法
下面是东哥总结的回溯法模板:
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
可以发现,前半部分因为是递归的因素,所以会不断在选择列表中加入元素,当递归全部结束的时候,会不断将原先加入的元素剔除,再进行递归,这个就类似于树的深度优先遍历。
vector<vector<int>> res;
vector<vector<int>> subsets(vector<int>& nums) {
// 记录走过的路径
vector<int> track;
backtrack(nums, 0, track);
return res;
}
void backtrack(vector<int>& nums, int start, vector<int>& track) {
res.push_back(track);
// 注意 i 从 start 开始递增
for (int i = start; i < nums.size(); i++) {
// 做选择
track.push_back(nums[i]);
// 回溯
backtrack(nums, i + 1, track);
// 撤销选择
track.pop_back();
}
}
将这个过程可视化:
由上图可以发现,每个节点都是子集,所以在继续访问节点之前,先将节点信息保存。进行回溯的条件为,判断可供选择元素的数量,体现在循环数组中,当没有元素可供选择的时候,for语句就会退出。这时候就会退出全部算法,或者是退出当前递归层。返回递归的时候,将向量中的元素弹出,并且更新选择列表(体现在递归函数的输入的参数)。注意这里的选择列表的选取方式,这里更新之后的选择列表就是当前标志指针所指的之后的所有元素。
全排列
每次都选取一个元素,并做好标记(防止元素被重复选取),然后重新遍历一遍所有元素,与之匹配,直到数组中的数量满足要求,将结果输出。
//处理排列的idex号位
void generateP(int index){
//这是递归边界,前面的元素全部排列完毕,现在是要将数组输出
if (index == n + 1) {
for (int i = 1; i <= n; ++i) {
printf("%d", p[i]);
}
printf("\n");
return;
}
//遍历检查所有的元素,这里引用了外部的变量n,由主函数定义
for (int x = 1; x <= n; ++x) {
//哈希表是来表示索引元素是否已经加入数组了
if (hashTable[x] == false) {
p[index] = x;//以x作为头部的时候
hashTable[x] = true;//更新状态
//这里是继续下一位,注意这里还没有将哈希表重置,所以原先作为首部的元素是不会再被赋值了
generateP(index + 1);
hashTable[x] = false;//完成递归项中的一个,重置状态
}
}
count++;
return;
}