算法基础整理
上传的目的一方面是备份笔记,另一方面也分享给大家。
一、常用函数及算法
(1)进制
1)十进制转R进制(2<= R<=36)
string DtoR(int n,int r)
{
string s = "";
int x;
if(n == 0)
{
s = "0";
}
while(n)
{
x = n % r;
n = n/r;
if(x < 10)
{
s = char(x +48) + s;
}
else
{
s = char(x - 10 +'A') + s;
}
}
return s;
}
2)R进制转十进制
int RtoD(string s, int r) // s为R进制数
{
int len = s.size();
int x, k = 0;
for (int i = 0; i < len; i++)
{
if (isupper(s[i]))
{
x = s[i] - 'A' + 10;
}
else
{
x = s[i] - '0';
}
k = k * r + x;
}
return k; // 十进制数
}
(2)判断素数
方法1
//判断素数
int judge(int n)
{
int i;
if(n<=1) return 0;
for(i = 2;i <= n/i;i++)//条件可改为i <= sqrt(n);
if( n % i == 0) return 0;
return 1;
}
方法2
//判断素数
int judge(int n)
{
int i;
if(n<=1) return 0;
for(i = 2;i <= sqrt(n);i++)
{
if( n % i == 0)
{
return 0;
}
}
return 1;
}
方法原理
正整数A,设它的平方根为Q
1、如果存在一个大于1小于Q的整数可以整除A,则必然存在一个大于Q小于A的整数可以整除A;
2、如果不存在一个大于1小于Q的整数可以整除A,则必然也不存在一个大于Q小于A的整数可以整除A。先证明第1条
设B是一个大于1小于Q的整数,且B可以整除A。按整除的定义存在一个整数C,使得 B * C = A
C必然大于Q。因为如果C小于等于Q,则B * C <= B * Q < Q * Q = A。这与整除定义相矛盾。再证明第2条
设C是一个整数,Q < C < A,且C可以整除A。按整除的定义存在一个整数B,使得 B * C = A
由于 Q < C 所以 B < Q(证明方法同上,不再赘述),这与题设2矛盾,所以这样的C不存在。证毕
(3)字符串
将一行未知长度的字符串读入字符数组(一般用EOF,用换行符在很多oj里会出现段错误(segmentation fault))
char num[100000];
char c;
int k = 0;
while((c = getchar()) != EOF)
{
num[k++] = c;
}
ASCII码转字符以及字符转ASCII码
for(int i = 0;i < k;i++)
{
int nn = (int)num[i]+4; //字符转ASCII码
cout<<(char)nn;//ASCII码转字符
}
也可以直接对字符串中的某个字符进行ASCII码加减,然后直接输出相应字符
#include<bits/stdc++.h>
using namespace std;
string s;
int main()
{
getline(cin, s);//读入一行字符串
int l = s.length();
for(int i = 0; i < l; i++) s[i] += 4;//直接对字符串中的某个字符进行ASCII码加减
cout << s;
return 0;
}
(4)统计字母出现次数(C++)
使用map容器进行统计,方便快捷
#include<bits/stdc++.h>
using namespace std;
int main()
{
map<char,int>s;//s是一个映射
char c;
do
{
cin>>c;
if(isalpha(c))//判断是字母
{
c = tolower(c);//变成小写
s[c]++;
}
}while(c != '#');
//迭代器访问容器
for(map<char,int>::iterator it = s.begin();it != s.end();++it)
{
cout<<it->first<<":"<<it->second<<endl;
}
return 0;
}
(5)常见ASCII码
- 数字0~9的ASCII对应值为 48 ~ 57
- 大写A ~Z的ASCII对应值为 65 ~ 90
- 小写a~z的ASCII对应值为 97 ~ 122
- 回车,ASCII码13
- 换行,ASCII码10
- 空格,ASCII码32
(6)字符串转整型
string s;
string res;
for(int i = 0;i < s.size();i++)
{
res += s[i] - '0';
}
(7)整型转字符串
to_string()
reverse(s.begin(),s.end())
(8)ToLower实现
string ToLower(string s)
{
string res="";
for(int i=0;i < s.size();i++)
{
if (s[i]>='A'&&s[i]<='Z')
res+=(s[i]+'a'-'A');
else
res+=s[i];
return res;
}
(9)String中find函数的用法
//find 函数 返回jk 在s 中的下标位置
position = s.find("jk");
if (position != s.npos) //如果没找到,返回一个特别的标志c++中用npos表示,我这里npos取值是4294967295,
//find 函数 返回flag 中任意字符 在s 中第一次出现的下标位置
flag = "c";
position = s.find_first_of(flag);
cout << "s.find_first_of(flag) is : " << position << endl;
//从字符串s 下标5开始,查找字符串b ,返回b 在s 中的下标
position=s.find("b",5);
cout<<"s.find(b,5) is : "<<position<<endl;
(10)substr的用法
str.substr(i,len);//截取字符串str从下标i开始长度为len的子串
(11)字符串分割方法(以任意长度空格分割,注意只能以空格分割)
string str;
string s[1000];
getline(cin,str);
stringstream input(str);
string result;
int k = 0;
while(input >> result)
s[k++] = result;
(12)x >> i & 1含义
判断x二进制表示中第i位是0还是1;
(13)运行时间复杂度分析
clock_t start,end;
start = clock();
end = clock();
cout<<(double)(end-start)/CLOCKS_PER_SEC<<endl;
(14)读取二维字符数组的方式(方便的一种)
char f[N][N];
for(int i = 0;i < n;i++)
cin>>f[i];
二、基础算法
(1)排序
1)冒泡排序
void bubbleSort(int a[], int n)
{
for(int i = n - 1; i > 0; i--)
for(int j = 0; j < i; j++)
if(a[j] > a[j+1])
swap(a[j], a[j+1]);
}
2)快速排序
#include <iostream>
using namespace std;
const int N = 100010;
int q[N];
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);
}
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);
quick_sort(q, 0, n - 1);
for (int i = 0; i < n; i ++ ) printf("%d ", q[i]);
return 0;
}
3)归并排序
利用分治的思想
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int a[N], tmp[N];
void merge_sort(int q[], int l, int r)
{
if (l >= r) return;//如果数据小于等于1个直接退出
int mid = l + r >> 1;//直接把最中间的值作为mid
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];
}
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; i ++ ) scanf("%d", &a[i]);
merge_sort(a, 0, n - 1);
for (int i = 0; i < n; i ++ ) printf("%d ", a[i]);
return 0;
}
(2)二分
模板1
while (l < r)
{
int mid = l + r >> 1; //(l+r)/2
if (check(mid)) r = mid; // check()判断mid是否满足性质
else l = mid + 1;
}
模板2
while (l < r)
{
int mid = l + r + 1 >> 1; //(l+r+1)/2
if (check(mid)) l = mid;
else r = mid - 1;
}
第一个模板是尽量往左找目标,第二个模板是尽量往右找目标。
只要是往左找答案,就用第一个模板,mid不用加一,r=mid,l减一;
只要是往右找答案,就用第二个模板,mid要加一,l=mid,r要减一;
(3)高精度
a)高精度加法
方法1
不压位代码
#include <iostream>
#include <vector>
using namespace std;
vector<int> add(vector<int> &A, vector<int> &B)
{
if (A.size() < B.size()) return add(B, A);
vector<int> C;
int t = 0;
for (int i = 0; i < A.size(); i ++ )
{
t += A[i];
if (i < B.size()) t += B[i];
C.push_back(t % 10);
t /= 10;
}
if (t) C.push_back(t);
return C;
}
int main()
{
string a, b;
vector<int> A, B;
cin >> a >> b;
for (int i = a.size() - 1; i >= 0; i -- ) A.push_back(a[i] - '0');
for (int i = b.size() - 1; i >= 0; i -- ) B.push_back(b[i] - '0');
auto C = add(A, B);
for (int i = C.size() - 1; i >= 0; i -- ) cout << C[i];
cout << endl;
return 0;
}
压9位的代码
#include <iostream>
#include <vector>
using namespace std;
const int base = 1000000000;
vector<int> add(vector<int> &A, vector<int> &B)
{
if (A.size() < B.size()) return add(B, A);
vector<int> C;
int t = 0;
for (int i = 0; i < A.size(); i ++ )
{
t += A[i];
if (i < B.size()) t += B[i];
C.push_back(t % base);
t /= base;
}
if (t) C.push_back(t);
return C;
}
int main()
{
string a, b;
vector<int> A, B;
cin >> a >> b;
for (int i = a.size() - 1, s = 0, j = 0, t = 1; i >= 0; i -- )
{
s += (a[i] - '0') * t;
j ++, t *= 10;
if (j == 9 || i == 0)
{
A.push_back(s);
s = j = 0;
t = 1;
}
}
for (int i = b.size() - 1, s = 0, j = 0, t = 1; i >= 0; i -- )
{
s += (b[i] - '0') * t;
j ++, t *= 10;
if (j == 9 || i == 0)
{
B.push_back(s);
s = j = 0;
t = 1;
}
}
auto C = add(A, B);
cout << C.back();
for (int i = C.size() - 2; i >= 0; i -- ) printf("%09d", C[i]);
cout << endl;
return 0;
}
方法2
#include<bits/stdc++.h>
using namespace std;
const int N = 100100;
int a[N],b[N];
string s1,s2;
void init(string s,int *v)
{
int n = s.size();
reverse(s.begin(),s.end());
v[0] = n;
for(int i = 0;i < n;i++) v[i+1] = s[i] - '0';
}
vector<int> add(int *a,int *b)
{
vector<int>c;
int t = 0,i = 0,x;
//i 小于a和b 的长度
while((i < a[0])||(i < b[0]))
{
i++;
x = a[i] + b[i] + t;
t = x / 10;
c.push_back(x % 10);
}
if(t > 0) c.push_back(t);
return c;
}
int main()
{
cin>>s1>>s2;
init(s1,a);
init(s2,b);
vector<int>res = add(a,b);
reverse(res.begin(),res.end());
for(int i = 0;i < res.size();i++) cout<<res[i];
cout<<endl;
return 0;
}
b)高精度减法
方法1
#include <iostream>
#include <vector>
using namespace std;
bool cmp(vector<int> &A, vector<int> &B)
{
if (A.size() != B.size()) return A.size() > B.size();
for (int i = A.size() - 1; i >= 0; i -- )
if (A[i] != B[i])
return A[i] > B[i];
return true;
}
vector<int> sub(vector<int> &A, vector<int> &B)
{
vector<int> C;
for (int i = 0, t = 0; i < A.size(); i ++ )
{
t = A[i] - t;
if (i < B.size()) t -= B[i];//t = A[i]-B[i]-t;t是进位
C.push_back((t + 10) % 10);//能解决A[i]比B[i]小的问题,也能解决A[i]比B[i]大的问题
if (t < 0) t = 1;//t < 0说明需要进行借位,否则不需要
else t = 0;
}
while (C.size() > 1 && C.back() == 0) C.pop_back();//删除前导0
return C;
}
int main()
{
string a, b;
vector<int> A, B;
cin >> a >> b;
for (int i = a.size() - 1; i >= 0; i -- ) A.push_back(a[i] - '0');//逆序保存在容器中
for (int i = b.size() - 1; i >= 0; i -- ) B.push_back(b[i] - '0');
vector<int> C;
//通过cmp比较A和B的大小,让大位减去小位
if (cmp(A, B)) C = sub(A, B);
else C = sub(B, A), cout << '-';
//逆序输出
for (int i = C.size() - 1; i >= 0; i -- ) cout << C[i];
cout << endl;
return 0;
}
方法2
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int a[N],b[N];
string s1,s2;
void init(string s,int *v)
{
int n = s.size();
v[0] = n;
reverse(s.begin(),s.end());
for(int i = 0;i < n;i++) v[i+1] = s[i]-'0';
}
vector<int> sub(int *a,int *b)
{
vector<int>c;
int i = 0,x;
while((i < a[0]) || (i < b[0]))
{
i++;
x = a[i]-b[i];
if(x < 0) x += 10,a[i+1]--;
c.push_back(x);
}
while(c.size() > 1&&c.back() == 0) c.pop_back();
return c;
}
int main()
{
cin>>s1>>s2;
if(s1.size() < s2.size()||(s1.size() == s2.size()&&s1 < s2))
{
cout<<"-";
swap(s1,s2);
}
init(s1,a);
init(s2,b);
vector<int>res = sub(a,b);
for(int i = res.size()-1;i >= 0;i--) cout<<res[i];
cout<<endl;
return 0;
}
c)高精度乘法
高精度数×低精度数
#include <iostream>
#include <vector>
using namespace std;
vector<int> mul(vector<int> &A, int b)
{
vector<int> C;
int t = 0;
for (int i = 0; i < A.size() || t; i++)
{
if (i < A.size()) t += A[i] * b;
C.push_back(t % 10);
t /= 10;
}
while (C.size() > 1 && C.back() == 0) C.pop_back();
return C;
}
//高精度数×低精度数
int main()
{
string a;
int b;
cin >> a >> b;
vector<int> A;
for (int i = a.size() - 1; i >= 0; i -- ) A.push_back(a[i] - '0');
auto C = mul(A, b);
for (int i = C.size() - 1; i >= 0; i -- ) printf("%d", C[i]);
return 0;
}
双精度方法1
高精度数×高精度数
#include<bits/stdc++.h>
using namespace std;
vector<int> mul(vector<int> &A,vector<int> &B)
{
vector<int>res;
int p = 0;
for(int j = 0;j < B.size();j++)
{
//cout<<p<<endl;
vector<int> C;
int t = 0;
for(int i = 0;i < A.size()||t;i++)
{
if(i < A.size()) t += A[i]*B[j];
C.push_back(t % 10);
t /= 10;
}
int temp = p;
if(p == 0)
{
for(long long i = 0;i < A.size()*B.size();i++)
res.push_back(0);
}
int k = 0;
for(int i = 0;i < C.size();i++)
{
//cout<<"C[i] = "<<C[i]<<endl;
k += res[temp] + C[i];
res[temp++] = (k%10);
k /= 10;
}
if(k != 0) res[temp] = k;
p++;
//cout<<p<<endl;
}
while (res.size() > 1 && res.back() == 0) res.pop_back();
return res;
}
int main()
{
string a,b;
cin>>a>>b;
vector<int> A;
vector<int> B;
for(int i = a.size()-1;i >= 0;i--) A.push_back(a[i] - '0');
for(int i = b.size()-1;i >= 0;i--) B.push_back(b[i] - '0');
auto C = mul(A,B);
for(int i = C.size()-1;i >= 0;i--) cout<<C[i];
return 0;
}
双精度方法2
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
vector<int>a,b;
string s1,s2;
void init(string s,vector<int> &v)
{
int n = s.size();
reverse(s.begin(),s.end());
for(int i = 0;i < n;i++) v.push_back(s[i]-'0');
}
vector<int> mul(vector<int>a,vector<int>b)
{
vector<int>c(a.size()+b.size());
for(int i = 0;i < a.size();i++)
for(int j = 0;j < b.size();j++)
c[i+j] += a[i]*b[j];
int n = a.size() + b.size();
for(int i = 0;i < n-1;i++)
{
c[i+1] += c[i] / 10;
c[i] = c[i] % 10;
}
while(c.size() > 1&&c.back() == 0) c.pop_back();
return c;
}
int main()
{
cin>>s1>>s2;
init(s1,a);
init(s2,b);
vector<int>res = mul(a,b);
for(int i = res.size()-1;i >= 0;i--) cout<<res[i];
cout<<endl;
return 0;
}
d)高精度除法
方法1
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
//A/B A是被除数,B 是除数,r 是余数
vector<int> div(vector<int> &A, int b, int &r)
{
vector<int> C;
r = 0;
for (int i = A.size() - 1; i >= 0; i -- )
{
r = r * 10 + A[i];
C.push_back(r / b);
r %= b;
}
reverse(C.begin(), C.end());//为了和其他三种运算相符合,可以一起进行运算,所以倒序存储
while (C.size() > 1 && C.back() == 0) C.pop_back();//去除前导0
return C;
}
int main()
{
string a;
vector<int> A;
int B;
cin >> a >> B;
for (int i = a.size() - 1; i >= 0; i -- ) A.push_back(a[i] - '0');
int r;
auto C = div(A, B, r);
for (int i = C.size() - 1; i >= 0; i -- ) cout << C[i];//倒序输出
cout << endl << r << endl;
return 0;
}
方法2
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
vector<int>a;int b;
vector<int>c;
string s1,s2;
void init(string s,vector<int> &v)
{
int n = s.size();
for(int i = 0;i < n;i++) v.push_back(s[i]-'0');
}
int div(vector<int> a,int b,vector<int> &c)
{
int t = 0,x;//返回余数
for(int i = 0;i < a.size();i++)
{
x = t * 10 + a[i];
c.push_back(x / b );
t = x % b;
}
reverse(c.begin(),c.end());
while(c.size() > 1&&c.back() == 0) c.pop_back();
return t;
}
int main()
{
cin>>s1>>b;
init(s1,a);
int r = div(a,b,c);
for(int i = c.size()-1;i >= 0;i--) cout<<c[i];
cout<<".";
for(int i = 0;i < 20;i++)//显示小数点后20位,不进行四舍五入
{
int x = r*10;
cout<<x/b;
r = x%b;
}
return 0;
}
(4)前缀和差分
1)前缀
一维前缀和
#include <iostream>
using namespace std;
const int N = 100010;
int n, m;
int a[N], s[N];//前缀和一般定义两个数组,一个为原数组,另一个为前n项和的数组。类比数列中的前n项和
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
//初始化前缀和数组
//s[i]数组代表的是前i个数之和,初始化s[0] = 0,区间【l,r】之和为s[r]-s[l-1]
for (int i = 1; i <= n; i ++ ) s[i] = s[i - 1] + a[i]; // 前缀和的初始化
while (m -- )
{
int l, r;
scanf("%d%d", &l, &r);
printf("%d\n", s[r] - s[l - 1]); // 区间和的计算
}
return 0;
}
二维前缀和
子矩阵的和(画矩阵图理解)
#include <iostream>
using namespace std;
const int N = 1010;
int n, m, q;
int s[N][N];
int main()
{
scanf("%d%d%d", &n, &m, &q);
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
scanf("%d", &s[i][j]);
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
s[i][j] += s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j];
while (q -- )
{
int x1, y1, x2, y2;//当使用万能头时,不能写在main函数外面可能冲突造成编译错误
scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
printf("%d\n", s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]);
}
return 0;
}
以下为二维前缀和的矩阵解释,框中数字代表坐标
行和列的下标都从1开始,防止初始化数组时遇到麻烦,处理要写的代码比较多
1,1 | 1,2 | 1,3 | 1,4 |
---|---|---|---|
2,1 | 2,2 | 2,3 | 2,4 |
3,1 | 3,2 | 3,3 | ,3,4 |
4,1 | 4,2 | 4,3 | 4,4 |
5,1 | 5,2 | 5,3 | 5,4 |
其中a[i][j] 代表原数组中第 i 行第 j 列的元素z
前缀和的求法是先按行再按列进行计算,即第一行计算完转向第二行,可以通过遍历原数组实现
s [i] [j] = s [i-1] [j] + s [i] [j-1] - s [i-1] j-1] + a[i] [j];//减是因为s[i-1] [j-1]这一块被加过两次了
求(x1,y1),(x2,y2)区间内矩阵和为多少
ans = s[x2] [y2] - s[x1 - 1] [y2] - s[x2] [y1 - 1] + s[x1 - 1] [y1 - 1];//加是因为s[i-1] [j-1]这一块被减过两次了
2)差分
一维差分
#include <iostream>
using namespace std;
const int N = 100010;
int n, m;
int a[N], b[N];
void insert(int l, int r, int c)
{
b[l] += c;
b[r + 1] -= c;
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
for (int i = 1; i <= n; i ++ ) insert(i, i, a[i]);
while (m -- )
{
int l, r, c;
scanf("%d%d%d", &l, &r, &c);
insert(l, r, c);
}
for (int i = 1; i <= n; i ++ ) b[i] += b[i - 1];
for (int i = 1; i <= n; i ++ ) printf("%d ", b[i]);
return 0;
}
二维差分
差分矩阵
#include <iostream>
using namespace std;
const int N = 1010;
int n, m, q;
int a[N][N], b[N][N];
void insert(int x1, int y1, int x2, int y2, int c)
{
b[x1][y1] += c;
b[x2 + 1][y1] -= c;
b[x1][y2 + 1] -= c;
b[x2 + 1][y2 + 1] += c;
}
int main()
{
scanf("%d%d%d", &n, &m, &q);
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
scanf("%d", &a[i][j]);
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
insert(i, j, i, j, a[i][j]);
while (q -- )
{
int x1, y1, x2, y2, c;
cin >> x1 >> y1 >> x2 >> y2 >> c;
insert(x1, y1, x2, y2, c);
}
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ )
b[i][j] += b[i - 1][j] + b[i][j - 1] - b[i - 1][j - 1];
for (int i = 1; i <= n; i ++ )
{
for (int j = 1; j <= m; j ++ ) printf("%d ", b[i][j]);
puts("");
}
return 0;
}
(5)双指针算法
最长连续不重复子序列
#include <iostream>
using namespace std;
const int N = 100010;
int n;
int q[N], s[N];
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);
int res = 0;
for (int i = 0, j = 0; i < n; i ++ )
{
s[q[i]] ++ ;
while (j < i && s[q[i]] > 1) s[q[j ++ ]] -- ;
res = max(res, i - j + 1);
}
cout << res << endl;
return 0;
}
数组元素的目标和
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int n, m, x;
int a[N], b[N];
int main()
{
scanf("%d%d%d", &n, &m, &x);
for (int i = 0; i < n; i ++ ) scanf("%d", &a[i]);
for (int i = 0; i < m; i ++ ) scanf("%d", &b[i]);
for (int i = 0, j = m - 1; i < n; i ++ )
{
while (j >= 0 && a[i] + b[j] > x) j -- ;
if (j >= 0 && a[i] + b[j] == x) cout << i << ' ' << j << endl;
}
return 0;
}
判断子序列
#include <iostream>
#include <cstring>
using namespace std;
const int N = 100010;
int n, m;
int a[N], b[N];
int main()
{
scanf("%d%d", &n, &m);
for (int i = 0; i < n; i ++ ) scanf("%d", &a[i]);
for (int i = 0; i < m; i ++ ) scanf("%d", &b[i]);
int i = 0, j = 0;
while (i < n && j < m)
{
if (a[i] == b[j]) i ++ ;
j ++ ;
}
if (i == n) puts("Yes");
else puts("No");
return 0;
}
(6)位运算
位运算基础
&
按位与
如果两个相应的二进制位都为1,则该位的结果值为1,否则为0
|
按位或
两个相应的二进制位中只要有一个为1,该位的结果值为1
^
按位异或
若参加运算的两个二进制位值相同则为0,否则为1
~
取反
~是一元运算符,用来对一个二进制数按位取反,即将0变1,将1
<<
左移
用来将一个数的各二进制位全部左移N位,右补0右移
将一个数的各二进制位右移N位,移到右端的低位被舍弃,对于无符号数, 高位补0
应用
1.技巧一:用于消去x的最后一位的1
x & (x-1)
x = 1100
x-1 = 1011
x & (x-1) = 1000
1.1.应用一 用O(1)时间检测整数n是否是2的幂次.
思路解析:N如果是2的幂次,则N满足两个条件。
1.N>0
2.N的二进制表示中只有一个1
一位N的二进制表示中只有一个1,所以使用N&(N-1)将唯一的一个1消去。
如果N是2的幂次,那么N&(N-1)得到结果为0,即可判断。
1.2.应用二 计算在一个 32 位的整数的二进制表示中有多少个 1
思路解析:
由 x & (x-1) 消去x最后一位知。循环使用x & (x-1)消去最后一位1,计算总共消去了多少次即可。
2.异或的运用
2.1排除偶次
示例:在一个整数数组中,仅存在一个不重复的数字,其余数字均出现两次(或偶数次),找出不重复数字。
// 异或方法:将所有整数异或,出现偶数次的整数会被抵消,最终留下不重复整数。
int result = 0;
for (int index = 0; index < numArray.length; index++) {
result = result ^ numArray[index];
}
return result;
2.2排除偶次变种
示例:将数字1-1000存放在一个大小为1001的数组中,其中只有一个数字重复出现两次,找出重复数字。
除重复部分外,剩余部分均出现偶数个1,因此,整体异或为0值
因此,将1001个整数依次异或运算,最终结果就是重复的数字,相当于重复数字与0进行异或,得到其本身。
int result = 0;
for (int index = 0; index < numArray.length; index++) {
result = result ^ numArray[index];
}
return result;
二进制中1的个数
#include <iostream>
using namespace std;
int main()
{
int n;
scanf("%d", &n);
while (n -- )
{
int x, s = 0;
scanf("%d", &x);
for (int i = x; i; i -= i & -i) s ++ ;
printf("%d ", s);
}
return 0;
}
(7)离散化
区间和
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef pair<int, int> PII;
const int N = 300010;
int n, m;
int a[N], s[N];
vector<int> alls;
vector<PII> add, query;
int find(int x)
{
int l = 0, r = alls.size() - 1;
while (l < r)
{
int mid = l + r >> 1;
if (alls[mid] >= x) r = mid;
else l = mid + 1;
}
return r + 1;
}
vector<int>::iterator unique(vector<int> &a)
{
int j = 0;
for (int i = 0; i < a.size(); i ++ )
if (!i || a[i] != a[i - 1])
a[j ++ ] = a[i];
// a[0] ~ a[j - 1] 所有a中不重复的数
return a.begin() + j;
}
int main()
{
cin >> n >> m;
for (int i = 0; i < n; i ++ )
{
int x, c;
cin >> x >> c;
add.push_back({x, c});
alls.push_back(x);
}
for (int i = 0; i < m; i ++ )
{
int l, r;
cin >> l >> r;
query.push_back({l, r});
alls.push_back(l);
alls.push_back(r);
}
// 去重
sort(alls.begin(), alls.end());
alls.erase(unique(alls), alls.end());
// 处理插入
for (auto item : add)
{
int x = find(item.first);
a[x] += item.second;
}
// 预处理前缀和
for (int i = 1; i <= alls.size(); i ++ ) s[i] = s[i - 1] + a[i];
// 处理询问
for (auto item : query)
{
int l = find(item.first), r = find(item.second);
cout << s[r] - s[l - 1] << endl;
}
return 0;
}
(8)区间合并
区间合并
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef pair<int, int> PII;
void merge(vector<PII> &segs)
{
vector<PII> res;
sort(segs.begin(), segs.end());
int st = -2e9, ed = -2e9;
for (auto seg : segs)
if (ed < seg.first)
{
if (st != -2e9) res.push_back({st, ed});
st = seg.first, ed = seg.second;
}
else ed = max(ed, seg.second);
if (st != -2e9) res.push_back({st, ed});
segs = res;
}
int main()
{
int n;
scanf("%d", &n);
vector<PII> segs;
for (int i = 0; i < n; i ++ )
{
int l, r;
scanf("%d%d", &l, &r);
segs.push_back({l, r});
}
merge(segs);
cout << segs.size() << endl;
return 0;
}
(9)三分
#include<bits/stdc++.h>
using namespace std;
const int N = 10000;
int n;
int arr[N];
int trisearch(int arr[],int n)
{
if(arr[0] == '\0') return -1;
int left = 0;
int right = n;
while(left < right)
{
int midl = left + (right - left)/3;
int midr = right - (right - left)/3;
if(arr[midl] > arr[midr])
right = midr - 1;
else
left = midl + 1;
}
return left;
}
int main()
{
//求最大值
int arr[] = {1,2,5,7,11,14,16,17,20,22,25,28,30,33,35,38,40,42,46,50,53,57,58,60, 55, 44, 33, 22, 11 , 10, 7, 6, 5, 1};
int n = 34;
int index = trisearch(arr,n);
if(index == -1)
cout<<-1<<endl;
else
cout<<arr[index];
return 0;
}
三、数据结构
(1)单链表(数组模拟)
#include <iostream>
using namespace std;
const int N = 100010;
// head 表示头结点的下标
// e[i] 表示节点i的值
// ne[i] 表示节点i的next指针是多少
// idx 存储当前已经用到了哪个点
int head, e[N], ne[N], idx;
// 初始化
void init()
{
head = -1;
idx = 0;
}
// 将x插到头结点
void add_to_head(int x)
{
e[idx] = x, ne[idx] = head, head = idx ++ ;
}
// 将x插到下标是k的点后面
void add(int k, int x)
{
e[idx] = x, ne[idx] = ne[k], ne[k] = idx ++ ;
}
// 将下标是k的点后面的点删掉
void remove(int k)
{
ne[k] = ne[ne[k]];
}
int main()
{
int m;
cin >> m;
init();
while (m -- )
{
int k, x;
char op;
cin >> op;
if (op == 'H')
{
cin >> x;
add_to_head(x);
}
else if (op == 'D')
{
cin >> k;
if (!k) head = ne[head];
else remove(k - 1);
}
else
{
cin >> k >> x;
add(k - 1, x);
}
}
for (int i = head; i != -1; i = ne[i]) cout << e[i] << ' ';
cout << endl;
return 0;
}
(2)双链表(数组模拟)
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+50;
int e[N],l[N],r[N],idx;
void init()
{
r[0] = 1;
l[1] = 0;
idx = 2;
}
//在k的右边插入x
void add(int k,int x)
{
e[idx] = x;//e用来存值,l,r数组用来存下标
r[idx] = r[k];
l[idx] = k;
l[r[k]] = idx;
r[k] = idx++;
}
//删除k节点
void remove(int k)
{
l[r[k]] = l[k];
r[l[k]] = r[k];
}
int main()
{
init();
int m;cin>>m;
while(m--)
{
string s;
int a = 0,b = 0;
cin>>s;
if(s == "L")
{
cin>>a;
add(0,a);
}
else if(s == "R")
{
cin>>a;
add(l[1],a);
}
else if(s == "D")
{
cin>>a;
remove(a+1);//注意下标从2开始所以此处用a+1
}
else if(s == "IR")
{
cin>>a>>b;
add(a+1,b);
}
else if(s == "IL")
{
cin>>a>>b;
add(l[a+1],b);
}
}
for(int i = r[0];i != 1;i = r[i]) cout<<e[i]<<" ";//遍历链表的方法
cout<<endl;
return 0;
}
(3)栈
#include<bits/stdc++.h>
using namespace std;
const int N = 100100;
int stk[N],tt;
int m;
int main()
{
cin>>m;
while(m--)
{
string op;
cin>>op;
int num;
if(op == "push")
{
cin>>num;
stk[++tt] = num;
}
else if(op == "pop")
{
tt--;
}
else if(op == "empty")
{
if(tt > 0) cout<<"NO"<<endl;
else cout<<"YES"<<endl;
}
else if(op == "query")
{
cout<<stk[tt]<<endl;
}
}
return 0;
}
(4)队列
数组模拟队列
操作:出队,入队,返回队头元素,返回队列是否为空;
#include<bits/stdc++.h>
using namespace std;
const int N = 100100;
int q[N],hh,tt = -1;//hh队头,tt队尾
int main()
{
int m;cin>>m;
while(m--)
{
string op;cin>>op;
if(op == "push")
{
int x;cin>>x;
q[++tt] = x;
}
else if(op == "pop")hh++;//从队头出队
else if(op == "empty")
{
cout<<(hh <= tt? "NO":"YES")<<endl;
}
else if(op == "query")
{
cout<<q[hh]<<endl;//返回队头元素
}
//cout<<q[tt]<<endl;返回队尾元素
}
return 0;
}
(5)单调栈
#include<bits/stdc++.h>
using namespace std;
//单调栈,栈中的元素始终是单调的
const int N = 100100;
int stk[N],tt;
int main()
{
int n;cin>>n;
while(n--)
{
int x;cin>>x;
while(tt&&stk[tt] >= x) tt--;//当输入一个比左侧数字还小的数字时,会把栈中比输入的x大的数字全部删除
if(tt) cout<<stk[tt]<<" ";
else cout<<-1<<" ";
stk[++tt] = x;
}
return 0;
}
(6)单调队列
题目:滑动窗口
#include<bits/stdc++.h>
using namespace std;
int n,k;
const int N = 1001000;
int a[N],q[N],hh,tt = -1;
int main()
{
cin>>n>>k;
for(int i = 0;i < n;i++) cin>>a[i];
for(int i = 0;i < n;i++)
{
if(hh <= tt&&(i - k + 1) > q[hh]) hh++;
//首先保证队列不为空。单调递增队列
while(hh <= tt&&a[q[tt]] >= a[i]) tt--;
q[++tt] = i;//单调队列中维护的是下标,下标对应的元素是单调的
if(i -k + 1>= 0) cout<<a[q[hh]]<<" ";
}
cout<<endl;
//重新初始化hh和tt
hh = 0;tt = -1;
//单调递减队列对称来写
for(int i = 0;i < n;i++)
{
if(hh <= tt&&(i - k + 1) > q[hh]) hh++;
//首先保证队列不为空。单调递减队列
while(hh <= tt&&a[q[tt]] <= a[i]) tt--;
q[++tt] = i;//单调队列中维护的是下标
if(i -k + 1>= 0) cout<<a[q[hh]]<<" ";
}
cout<<endl;
return 0;
}
(7)KMP
参考博客:https://blog.csdn.net/v_JULY_v/article/details/7041827
题目:KMP字符串
#include<bits/stdc++.h>
using namespace std;
const int N = 100100;
const int M = 1001000;
int n,m;
char s[M],p[N];
int ne[N];
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n>>p+1>>m>>s+1;
//求next数组
for(int i = 2,j = 0;i <= n;i++)
{
while(j&&p[i] != p[j+1]) j = ne[j];
if(p[i] == p[j+1]) j++;
ne[i] = j;
}
//下标从1开始
for(int i = 1,j = 0;i <= m;i++)
{
while(j&&s[i] != p[j+1])j = ne[j];
if(s[i] == p[j+1]) j++;
if(j == n)
{
cout<<i-n<<" ";
j = ne[j];
}
}
return 0;
}
(8)Trie(字典树)
字典树(又叫单词查找树、TrieTree),是一种树形结构,典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串,如01字典树)。主要思想是利用字符串的公共前缀来节约存储空间。很好地利用了串的公共前缀,节约了存储空间。字典树主要包含两种操作,插入和查找。
#include<bits/stdc++.h>
using namespace std;
const int N = 100100;
int son[N][26],cnt[N],idx;
char str[N];
//字典树啊啊啊啊~~~
void insert(char str[])
{
int p = 0;
for(int i = 0;str[i];i++)
{
int u = str[i]-'a';
if(!son[p][u]) son[p][u] = ++idx;
p = son[p][u];
}
cnt[p]++;
}
int query(char str[])
{
int p = 0;
for(int i = 0;str[i];i++)
{
int u = str[i] - 'a';
if(!son[p][u]) return 0;//没找到
p = son[p][u];//在trie树中,p是唯一的
}
return cnt[p];
}
int main()
{
int n;cin>>n;
while(n--)
{
string s;cin>>s;
if(s == "I")
{
cin>>str;
insert(str);
}
else
{
cin>>str;
cout<<query(str)<<endl;
}
}
return 0;
}
(9)并查集
合并集合
#include<bits/stdc++.h>
using namespace std;
const int N = 1000100;
int t[N],n,m;
int find(int x)//返回根节点+路径压缩;
{
if(t[x] != x) t[x] = find(t[x]);
return t[x];
}
int main()
{
cin>>n>>m;
for(int i = 1;i <= n;i++) t[i] = i;
while(m--)
{
string s;
int a,b;
cin>>s>>a>>b;
if(s == "M") t[find(a)] = find(b);
else{
if(find(a) == find(b)) puts("Yes");
else puts("No");
}
}
return 0;
}
连通块中点的数量
#include<bits/stdc++.h>
using namespace std;
const int N = 100100;
int t[N],cnt[N];
int n,m;
int find(int x)
{
if(t[x] != x) t[x] = find(t[x]);
return t[x];
}
int main()
{
cin>>n>>m;
for(int i = 1;i <= n;i++)
{
t[i] = i;
cnt[i] = 1;
}
while(m--)
{
string s;
cin>>s;
int a,b;
if(s == "C")
{
cin>>a>>b;
a = find(a),b =find(b);
if(a != b)
{
t[a] = b;//这一步之后把a的集合插入b
cnt[b] += cnt[a];//这里必须用原来的find(a),上一步后find(a)已改变
}
}
else if(s == "Q1")
{
cin>>a>>b;
if(find(a) == find(b))
cout<<"Yes"<<endl;
else
cout<<"No"<<endl;
}
else{
cin>>a;
cout<<cnt[find(a)]<<endl;
}
}
return 0;
}
(10)堆
利用完全二叉树实现
手写堆
#include<bits/stdc++.h>
using namespace std;
const int N = 100100;
int n,m;
int h[N],cnt;
void down(int x)
{
int u = x;//完全二叉树越往下越小
if(x*2 <= cnt&&h[x*2] < h[u]) u = x * 2;//左子树比自己还小,则下标更新
if((x*2+1) <= cnt&&h[x*2+1] < h[u]) u = x*2+1;//右子树比上一次比较中的较小者还小,则下标更新
//找三个中的最小值
if(x != u)
{
swap(h[x],h[u]);
down(u);
}
}
int main()
{
cin>>n>>m;
for(int i = 1;i <= n;i++) cin>>h[i];
cnt = n;
//建堆O(N)的建法
for(int i = n/2;i;i--) down(i);
//因为完全二叉树除了最后一层,前面有n/2的数
while(m--)
{
cout<<h[1]<<" ";
h[1] = h[cnt--];
down(1);//down函数中处理的都是下标
}
cout<<endl;
return 0;
}
模拟堆
#include<bits/stdc++.h>
using namespace std;
const int N = 100100;
int h[N],ph[N],hp[N],cnt;
void heap_swap(int a,int b)
{
swap(ph[hp[a]],ph[hp[b]]);
swap(hp[a],hp[b]);
swap(h[a],h[b]);
}
void down(int u)
{
int t = u;
if(u*2 <= cnt&&h[u*2] < h[t]) t = u*2;
if((u*2+1) <= cnt&&h[u*2+1] < h[t]) t = u*2+1;
if(u != t)
{
heap_swap(u,t);
down(t);
}
}
void up(int u)
{
while(u/2&&h[u] < h[u/2])
{
heap_swap(u,u/2);
u >>= 1;//u除2然后不停循环,直到无法再上升
}
}
int main()
{
int n;cin>>n;
int m = 0;
while(n--)
{
string s;int k,x;
cin>>s;
if(s == "I")
{
cin>>x;
cnt++;
m++;
ph[m] = cnt;hp[cnt] = m;//建立映射关系
h[cnt] = x;
up(cnt);
}
else if(s == "PM")
{
cout<<h[1]<<endl;
}
else if(s == "DM")
{
heap_swap(1,cnt);
cnt--;
down(1);
}
else if(s == "D")
{
cin>>k;
k = ph[k];
heap_swap(k,cnt);
cnt--;
down(k);
up(k);
}
else
{
cin>>k>>x;
k = ph[k];
h[k] = x;
up(k);
down(k);
}
}
return 0;
}
(11)哈希表
哈希表又称散列表,散列函数,可以实现在O(1)的时间进行查找操作,这是将一个大范围映射到一个小范围中。
因此可能会出现冲突,即两个不同的数对应到同一个数上,为了避免这种情况,提出了两种方法,主要利用这两种方法解决。
1、开放寻址法
利用单个数组,映射到N的范围内,因为冲突概率比较低,并且冲突的数比较少,所以将数组的长度开成两倍(即2*N)
2、拉链法
有点像邻接图
模拟散列表
开放寻址法
#include<bits/stdc++.h>
using namespace std;
const int N = 200003,null = 0x3f3f3f3f;//0x3f3f3f3f是无穷大
int h[N],n;
//开放寻址法
int find(int x)
{
int t = (x%N + N) % N;
while(h[t] != null&&h[t] != x)
{
t++;
if(t == N) t = 0;
}
return t;
}
int main()
{
cin>>n;
memset(h,0x3f,sizeof h);//初始化h
while(n--)
{
string s;
int x;
cin>>s>>x;
if(s == "I") h[find(x)] = x;
else{
if(h[find(x)] == null) puts("No");
else puts("Yes");
}
}
return 0;
}
拉链法
#include<bits/stdc++.h>
using namespace std;
const int N = 100003;
int h[N],e[N],ne[N],idx;
//h头节点,e存值,ne存下一个数的地址
void insert(int x)
{
int k = (x % N + N)%N;
//插入到链表中
e[idx] = x;
ne[idx] = h[k];
h[k] = idx++;
}
bool find(int x)
{
int k = (x % N + N)%N;
for(int i = h[k];i != -1;i = ne[i])
{
if(e[i] == x) return true;
}
return false;
}
int main()
{
int n;cin>>n;
memset(h,-1,sizeof h);
while(n--)
{
string s;int num;
cin>>s>>num;
if(s == "I") insert(num);
else {
if(find(num)) cout<<"Yes"<<endl;
else cout<<"No"<<endl;
}
}
return 0;
}
四、搜索与图论
(1)DFS(深度优先遍历算法)
DFS需要用到递归和回溯
排列数字问题
方法1:
#include<bits/stdc++.h>
using namespace std;
const int N = 10;
int n,path[N],st[N];
void dfs(int u)
{
if(u == n)
{
for(int i = 0;i < n;i++)
{
cout<<path[i]<<" ";
}
cout<<endl;
return;
}
for(int i = 1;i <= n;i++)
{
if(!st[i])
{
path[u] = i;
st[i] = true;
dfs(u+1);//递归到树的下一层
st[i] = false;//恢复
}
}
}
int main()
{
cin>>n;
dfs(0);
return 0;
}
方法2
#include <iostream>
using namespace std;
const int N = 10;
int n;
int path[N];
void dfs(int u, int state)
{
if (u == n)
{
for (int i = 0; i < n; i ++ ) printf("%d ", path[i]);
puts("");
return;
}
for (int i = 0; i < n; i ++ )
if (!(state >> i & 1))
{
path[u] = i + 1;
dfs(u + 1, state + (1 << i));
}
}
int main()
{
scanf("%d", &n);
dfs(0, 0);
return 0;
}
n-皇后问题
深搜,按行搜索
#include<bits/stdc++.h>
using namespace std;
const int N = 20;
char g[N][N];
int n;
bool col[N],dg[N],udg[N];
//按行搜索
void dfs(int u)
{
if(u == n)
{
for(int i = 0;i < n;i++)
{
for(int j = 0;j < n;j++)
{
cout<<g[i][j];
}
cout<<endl;
}
return;
}
for(int i = 0;i < n;i++)
{
if(!col[i]&&!dg[u+i]&&!udg[n-u+i])
{
g[u][i] = 'Q';
col[i] = dg[u+i] = udg[n-u+i] = true;
dfs(u+1);
col[i] = dg[u+i] = udg[n-u+i] = false;
g[u][i] = '.';
}
}
}
int main()
{
cin>>n;
for(int i = 0;i < n;i++)
for(int j = 0;j < n;j++)
g[i][j] = '.';
dfs(0);
return 0;
}
按位深搜
#include<bits/stdc++.h>
using namespace std;
const int N = 20;
int n;
char g[N][N];
bool col[N],row[N],dg[N],udg[N];//主对角线,副对角线
//dg,udg下标存的是截距,副对角线y = x + b;b = y - x;为了使b大于0加一个偏移量n
//b = y - x + n;主对角线y = -x + b;b = x + y;
void dfs(int x,int y,int s)
{
if (s > n) return;
if(y == n) y = 0,x++;//当y走到边界时
if(x == n)//x已经到最后一行了,x指行,y指列
{
if(s == n)
{
for(int i = 0;i < n;i++)
{
for(int j = 0;j < n;j++)
{
cout<<g[i][j];
}
cout<<endl;
}
cout<<endl;
}
return;
}
dfs(x,y+1,s);
if(!row[x]&&!col[y]&&!dg[x + y]&&!udg[n-y+x])
{
g[x][y] = 'Q';
row[x] = col[y] = dg[x + y] = udg[n-y+x] = true;
dfs(x,y+1,s+1);
g[x][y] = '.';
row[x] = col[y] = dg[x + y] = udg[n-y+x] = false;
}
}
int main()
{
cin>>n;
for(int i = 0;i < n;i++)
for(int j = 0;j < n;j++)
g[i][j] = '.';
dfs(0,0,0);
return 0;
}
(2)BFS(广度优先遍历算法)
走迷宫
#include<bits/stdc++.h>
using namespace std;
const int N = 110;
typedef pair<int,int>PII;
int n,m;
int g[N][N],d[N][N];
//BFS
int bfs()
{
queue<PII>q;
memset(d,-1,sizeof d);
q.push({0,0});
d[0][0] = 0;
int dx[4] = {-1,0,1,0};int dy[4] = {0,1,0,-1};
while(q.size())
{
auto t = q.front();
q.pop();
//开始向四个方向走
for(int i = 0;i < 4;i++)
{
int x = t.first + dx[i];int y = t.second + dy[i];
//判断x,y是否越界
if(x >= 0&&x < n&&y >= 0&&y < m&&g[x][y] == 0&&d[x][y] == -1)
{
d[x][y] = d[t.first][t.second]+1;
q.push({x,y});
}
}
}
return d[n-1][m-1];
}
int main()
{
cin>>n>>m;
for(int i = 0;i < n;i++)
for(int j = 0;j < m;j++)
cin>>g[i][j];
cout<<bfs()<<endl;
return 0;
}
八数码(BFS经典问题)
#include<bits/stdc++.h>
using namespace std;
int bfs(string state)
{
queue<string> q;
unordered_map<string,int> d;
q.push(state);
d[state] = 0;
string end = "12345678x";
while(q.size())
{
auto t = q.front();
q.pop();
int dis = d[t];
if(t == end) return dis;
int dx[4] = {-1,0,1,0},dy[4] = {0,1,0,-1};
int k = t.find('x');
int x = k / 3,y = k % 3;
for(int i = 0;i < 4;i++)
{
int a = x + dx[i],b = y + dy[i];
if(a >= 0&&a < 3&&b >=0&&b < 3)
{
swap(t[a*3+b],t[k]);
if(!d.count(t))
{
d[t] = dis+1;
q.push(t);
}
swap(t[a*3+b],t[k]);
}
}
}
return -1;
}
int main()
{
string s;
string state;
for(int i = 0;i < 9;i++)
{
cin>>s;
state += s;
}
cout<<bfs(state)<<endl;
return 0;
}
(3)树与图的深度优先遍历
树的重心
#include<bits/stdc++.h>
using namespace std;
const int N = 100100,M = N*2;
int n,e[M],h[M],ne[M],idx;
int ans = N;
bool st[N];
void add(int a,int b)
{
//在a后面插入b
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
int dfs(int u)
{
st[u] = true;
int sum = 0,size = 0;//sum代表每一个子树之和
for(int i = h[u];i != -1;i = ne[i])
{
int j = e[i];
if(st[j]) continue;//如果已经搜索过,则跳过
int s = dfs(j);//一直搜到叶节点
size = max(size,s);
sum += s;
}
size = max(size,n - sum -1);//n-sum-1 --代表当除了当前节点以及当前节点的子树
ans = min(ans,size);//最大值的最小值
return sum+1;
}
int main()
{
cin>>n;
memset(h,-1,sizeof h);
for(int i = 0;i < n-1;i++)
{
int a,b;cin>>a>>b;
add(a,b);add(b,a);
}
dfs(1);
cout<<ans;
return 0;
}
(4)树与图的广度优先遍历
图中点的层次
#include<bits/stdc++.h>
using namespace std;
const int N = 100100;
int n,m,d[N];
int h[N],e[N],ne[N],idx;
//用邻接表的方式存储图
void add(int a,int b)
{
e[idx] = b;ne[idx] = h[a];h[a] = idx++;
}
int bfs()
{
queue<int> q;
memset(d,-1,sizeof d);
q.push(1);
d[1] = 0;
while(q.size())
{
int t = q.front();
q.pop();
for(int i = h[t];i != -1;i = ne[i])
{
int j = e[i];
if(d[j] == -1)
{
d[j] = d[t]+1;//j位于第几层,即为最短需要多远才能到
q.push(j);
}
}
}
return d[n];
}
int main()
{
cin>>n>>m;
memset(h,-1,sizeof h);
for(int i = 0;i < m;i++)
{
int a,b;cin>>a>>b;
add(a,b);
}
cout<<bfs()<<endl;
return 0;
}
(5)拓扑排序
若一个由图中所有点构成的序列 A 满足:对于图中的每条边 (x,y),x在 A 中都出现在 y 之前,则称 A 是该图的一个拓扑序列。
拓扑排序只针对有向图,如果图中存在环,则一定不存在拓扑序。
简单说就是排好序后所有节点都是由前面的节点指向后面的节点。
有向图的拓扑序列
#include<bits/stdc++.h>
using namespace std;
const int N = 100100;
//利用邻接表存储图
int h[N],e[N],ne[N],idx;
int d[N];//存储所有点的入度数
int q[N];//存储最后排好序的序列
int n,m;
void add(int a,int b)
{
e[idx] = b;ne[idx] = h[a];h[a] = idx++;
}
bool topsort()
{
int hh = 0,tt = -1;
for(int i = 1;i <= n;i++)//1-n个节点
if(!d[i])
q[++tt] = i;
while(hh <= tt)
{
int t = q[hh++];//逻辑删除,实际上原来的数还在
for(int i = h[t];i != -1;i = ne[i])//枚举t到j的每一条边
{
int j = e[i];
d[j]--;
if(d[j] == 0)//如果j的入度为0就可以加入到队列中,这样即满足拓扑排序
q[++tt] = j;
}
}
return tt == n-1;//tt == n-1代表已经遍历完所有节点,否则没有,不满足条件一般是自环
}
int main()
{
cin>>n>>m;
memset(h,-1,sizeof h);
for(int i = 0;i < m;i++)
{
int a,b;cin>>a>>b;
add(a,b);
d[b]++;
}
if(topsort())
{
for(int i = 0;i < n;i++)
{
cout<<q[i]<<" ";
}
cout<<endl;
}
else{
cout<<-1<<endl;
}
return 0;
}
(6)Dijkstra
最短路算法归纳
- 单源最短路
- 所有边权为正数
- 朴素版dijkstra(O(n*n))
- 堆优化版dijkstra(O(mlogn))
- 存在负权边
- Bellmen-Ford(O(nm))
- SPFA(一般为O(m) 最坏为O(nm))
- 多源汇最短路
- Floyd算法(O(n^3))
经典的最短路算法
朴素版dijkstra(O(n*n))
#include<bits/stdc++.h>
using namespace std;
const int N = 510;
int g[N][N];//邻接矩阵存储
bool st[N];
int dist[N];
int n,m;
int dijkstra()
{
memset(dist,0x3f,sizeof dist);
dist[1] = 0;
for(int i = 0;i < n-1;i++)
{
int t = -1;
for(int j = 1;j <= n;j++)
{
if(!st[j]&&(t == -1||dist[t] > dist[j]))
{
t = j;//找到离已确定点最小的距离的点
}
}
for(int j = 1;j <= n;j++)
{
dist[j] = min(dist[j],dist[t]+g[t][j]);
}
st[t] = true;//代表t号点已经确定了
}
if(dist[n] == 0x3f3f3f3f) return -1;
else return dist[n];
}
int main()
{
cin>>n>>m;
memset(g,0x3f,sizeof g);
while(m--)
{
int a,b,c;
cin>>a>>b>>c;
g[a][b] = min(g[a][b],c);
}
cout<<dijkstra()<<endl;
return 0;
}
堆优化版dijkstra(O(mlogn))
#include<bits/stdc++.h>
using namespace std;
const int N = 1001000;//邻接表存储,因为邻接矩阵开不了那么大
//堆优化版
int n,m;
int h[N],e[N],w[N],ne[N],idx;//w为权重
int dist[N];
bool st[N];
typedef pair<int,int> PII;
void add(int a,int b,int c)
{
e[idx] = b;w[idx] = c;ne[idx] = h[a];h[a] = idx++;
}
int dijkstra()
{
memset(dist,0x3f,sizeof dist);
dist[1] = 0;//设置1号节点权重为0,dist存储权重
priority_queue<PII,vector<PII>,greater<PII>> heap;//默认是大根堆,此处为小根堆 根节点小于叶节点
heap.push({0,1});//第一个点的权重为0
while(heap.size())
{
auto t = heap.top();
heap.pop();
int ver = t.second;int distance = t.first;
if(st[ver]) continue;
st[ver] = true;
for(int i = h[ver];i != -1;i = ne[i])
{
int j = e[i];
if(dist[j] > dist[ver] + w[i])
{
dist[j] = dist[ver] + w[i];//选择权重更小的
heap.push({dist[j],j});
}
}
}
if(dist[n] == 0x3f3f3f3f) return -1;
else return dist[n];
}
int main()
{
cin>>n>>m;
memset(h,-1,sizeof h);//初始化h
while(m--)
{
int a,b,c;cin>>a>>b>>c;
add(a,b,c);
}
cout<<dijkstra()<<endl;
return 0;
}
(7)bellman-ford
有边数限制的最短路
#include<bits/stdc++.h>
using namespace std;
const int N = 510,M = 10010;
int dist[N],last[N];//last存上一次的dist
int n,m,k;
struct Edge{
int a,b,w;
}edges[M];//m条边
int bellmen_ford()
{
memset(dist,0x3f,sizeof dist);
dist[1] = 0;
for(int i = 0;i < k;i++)
{
memcpy(last,dist,sizeof dist);
for(int j = 0;j < m;j++)
{
auto e = edges[j];
dist[e.b] = min(dist[e.b],last[e.a]+e.w);
}
}
if(dist[n] > 0x3f3f3f3f/2) return -1;
else return dist[n];
}
int main()
{
cin>>n>>m>>k;
for(int i = 0;i < m;i++)
{
int a,b,w;
cin>>a>>b>>w;
edges[i] = {a,b,w};
}
int t = bellmen_ford();
if(t == -1) cout<<"impossible"<<endl;
else cout<<t<<endl;
return 0;
}
(8)spfa
spfa求最短路
属于bellmen_ford的优化版,和堆优化版的dijkstra算法求最短路很像
#include<bits/stdc++.h>
using namespace std;
const int N = 100100;
int h[N],e[N],w[N],ne[N],idx;
int n,m;
int dist[N];
bool st[N];
void add(int a,int b,int c)
{
e[idx] = b;w[idx] = c;ne[idx] = h[a];h[a] = idx++;
}
int spfa()
{
memset(dist,0x3f,sizeof dist);
dist[1] = 0;
queue<int> q;
q.push(1);
st[1] = true;
while(q.size())
{
int t = q.front();
q.pop();
st[t] = false;
for(int i = h[t];i != -1;i = ne[i])
{
int j = e[i];
if(dist[j] > dist[t] + w[i])
{
dist[j] = dist[t]+w[i];
if(!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
if(dist[n] == 0x3f3f3f3f) return -1;
else return dist[n];
}
int main()
{
cin>>n>>m;
memset(h,-1,sizeof h);
while(m--)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
int t = spfa();
if(t == -1) cout<<"impossible"<<endl;
else cout<<t<<endl;
return 0;
}
spfa判断负环
判断负环的原理:抽屉原理
dist[i] >= n;说明到达i经过n+1个点,实际上只有n个点,所以存在负环
#include<bits/stdc++.h>
using namespace std;
const int N = 100100;
int h[N],e[N],w[N],ne[N],idx;
int n,m;
int dist[N],cnt[N];
bool st[N];
void add(int a,int b,int c)
{
e[idx] = b;w[idx] = c;ne[idx] = h[a];h[a] = idx++;
}
int spfa()
{
queue<int> q;
//要判断有没有负环,所以把每个点都放进来,可能从1号点到不了负环
for(int i = 1;i <= n;i++)
{
st[i] = true;
q.push(i);
}
while(q.size())
{
int t = q.front();
q.pop();
st[t] = false;
for(int i = h[t];i != -1;i = ne[i])
{
int j = e[i];
if(dist[j] > dist[t] + w[i])
{
dist[j] = dist[t]+w[i];
cnt[j] = cnt[t] + 1;
if(cnt[j] >= n) return true;
if(!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
return false;
}
int main()
{
cin>>n>>m;
memset(h,-1,sizeof h);
while(m--)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
if(spfa()) cout<<"Yes"<<endl;
else cout<<"No"<<endl;
return 0;
}
(9)Floyd
Floyd求最短路
#include<bits/stdc++.h>
using namespace std;
const int N = 1000,INF = 1e9;
int d[N][N];
int n,m,k;
void floyd()
{
for(int k = 1;k <= n;k++)
for(int i = 1;i <= n;i++)
for(int j = 1;j <= n;j++)
d[i][j] = min(d[i][j],d[i][k]+d[k][j]);
}
int main()
{
cin>>n>>m>>k;
for(int i = 1;i <= n;i++)
for(int j = 1;j <= n;j++)
if(i == j) d[i][j] = 0;
else d[i][j] = INF;
while(m--)
{
int a,b,w;
cin>>a>>b>>w;
d[a][b] = min(d[a][b],w);
}
floyd();
while(k--)
{
int x,y;
cin>>x>>y;
if(d[x][y] > INF/2) puts("impossible");
else cout<<d[x][y]<<endl;
}
return 0;
}
(10)Prim
Prim算法求最小生成树
#include<bits/stdc++.h>
using namespace std;
const int N = 510,INF = 0x3f3f3f3f;
int g[N][N];
bool st[N];
int dist[N];
int n,m;
int prim()
{
memset(dist,0x3f,sizeof dist);
int res = 0;
for(int i = 0;i < n;i++)
{
//先找到离集合最小的点,然后用这个点去更新其他点到集合的距离
int t = -1;
for(int j = 1;j <= n;j++)
if(!st[j]&&(t == -1||dist[t] > dist[j]))
t = j;
if(i&&dist[t] == INF) return INF;
if(i) res += dist[t];//如果不是第一点则将权重相加,这个点放入集合中
//必须先把权重相加,再去更新其他点,因为在更新的过程中dist[t]可能会发生变化
for(int j = 1;j <= n;j++)
dist[j] = min(dist[j],g[t][j]);
st[t] = true;//记录t已经加入集合了
}
return res;
}
int main()
{
cin>>n>>m;
memset(g,0x3f,sizeof g);
while(m--)
{
int a,b,c;
cin>>a>>b>>c;
g[a][b] = g[b][a] = min(g[a][b],c);
}
//求最小权重之和
int t = prim();
if(t == INF) cout<<"impossible"<<endl;
else cout<<t<<endl;
}
(11)Kruskal
Kruskal算法求最小生成树
#include<bits/stdc++.h>
using namespace std;
const int N = 1001000,INF = 0x3f3f3f3f;
int n,m;
int p[N];
struct Edge{
int a,b,w;
//运算符重载,重载小于号
bool operator < (const Edge &W)const{
return w < W.w;
}
}edges[N];
int find(int x)
{
if(p[x] != x) p[x] =find(p[x]);
return p[x];
}
int kruskal()
{
sort(edges,edges+m);
//初始化并查集
for(int i = 1;i <= n;i++)p[i] = i;
int res = 0,cnt = 0;
for(int i = 0;i < m;i++)
{
int a = edges[i].a;
int b = edges[i].b;
int w = edges[i].w;
a = find(a),b = find(b);
if(a != b)
{
p[a] = b;
res += w;
cnt++;
}
}
if(cnt < n-1) return INF;
else return res;
}
int main()
{
cin>>n>>m;
for(int i = 0;i < m;i++)
{
int a,b,w;
cin>>a>>b>>w;
edges[i] = {a,b,w};
}
int t = kruskal();
if(t == INF) puts("impossible");
else cout<<t<<endl;
return 0;
}
(12)染色法判定二分图
染色法判定二分图
#include<bits/stdc++.h>
using namespace std;
const int N = 100100,M = 200200;
int h[N],e[M],ne[M],idx;
int n,m;
int color[N];
void add(int a,int b)
{
e[idx] = b;ne[idx] = h[a];h[a] = idx++;
}
bool dfs(int u,int c)
{
color[u] = c;
//同一棵树下的子节点
for(int i = h[u];i != -1;i = ne[i])
{
int j = e[i];
if(!color[j])
{
if(!dfs(j,3-c)) return false;
}
else if(color[j] == c) return false;
}
return true;
}
int main()
{
//邻接表存稀疏图,邻接矩阵存稠密图
cin>>n>>m;
memset(h,-1,sizeof h);
while(m--)
{
int a,b;
cin>>a>>b;
add(a,b);
add(b,a);
}
bool flag = true;
for(int i = 1;i<= n;i++)
{
if(!color[i])
{
if(!dfs(i,1))
{
flag = false;
break;
}
}
}
if(flag) cout<<"Yes"<<endl;
else cout<<"No"<<endl;
return 0;
}
(13)匈牙利算法
二分图的最大匹配
#include<bits/stdc++.h>
using namespace std;
const int N = 510,M = 100100;
int n1,n2,m;
int h[N],e[M],ne[M],idx;
int match[N];
bool st[N];
void add(int a,int b)
{
e[idx] = b;ne[idx] = h[a];h[a] = idx++;
}
bool find(int x)
{
//遍历所有与x相连的点,看是否有满足条件的
for(int i = h[x];i != -1;i = ne[i])
{
int j = e[i];
if(!st[j])
{
st[j] = true;
if(match[j] == 0||find(match[j]))
{
match[j] = x;
return true;
}
}
}
return false;
}
int main()
{
cin>>n1>>n2>>m;
memset(h,-1,sizeof h);
while(m--)
{
int a,b;
cin>>a>>b;
add(a,b);
}
int res = 0;
for(int i = 1;i <= n1;i++)
{
memset(st,false,sizeof st);//这里一定要初始化,即使是已经匹配过的,也可以再选然后递归
//查看是否可以让原匹配的点进行调换,如果可以,那么也算匹配成功
if(find(i)) res++;
}
cout<<res;
return 0;
}
五、数学知识
(1)质数
筛质数
筛法
1)朴素筛(埃氏筛)
#include <iostream>
#include <algorithm>
using namespace std;
const int N= 1000010;
int primes[N], cnt;
bool st[N];
void get_primes(int n)
{
for (int i = 2; i <= n; i ++ )
{
if (st[i]) continue;
primes[cnt ++ ] = i;
for (int j = i + i; j <= n; j += i)
st[j] = true;
}
}
int main()
{
int n;
cin >> n;
get_primes(n);
cout << cnt << endl;
return 0;
}
2)线性筛
#include <iostream>
#include <algorithm>
using namespace std;
const int N= 1000010;
int primes[N], cnt;
bool st[N];
void get_primes(int n)
{
for (int i = 2; i <= n; i ++ )
{
if (!st[i]) primes[cnt ++ ] = i;
for (int j = 0; primes[j] <= n / i; j ++ )
{
st[primes[j] * i] = true;
if (i % primes[j] == 0) break;
}
}
}
int main()
{
int n;
cin >> n;
get_primes(n);
cout << cnt << endl;
return 0;
}
(2)约数
试除法求约数
#include<bits/stdc++.h>
using namespace std;
vector<int> get_divisors(int n)
{
vector<int> res;
for(int i = 1;i <= n / i;i++)
{
if(n % i == 0)
{
res.push_back(i);
if(i != n / i) res.push_back(n / i);
}
}
sort(res.begin(),res.end());
return res;
}
int main()
{
int x;
cin>>x;
while(x--)
{
int a;
cin>>a;
auto ans = get_divisors(a);
for(auto t : ans) cout<<t<<" ";
cout<<endl;
}
}
约数个数
#include<bits/stdc++.h>
using namespace std;
const int mod = 1e9+7;
typedef long long LL;
int main()
{
int n;
cin>>n;
unordered_map<int,int>primes;
while(n--)
{
int x;
cin>>x;
for(int i = 2;i <= x/i;i++)
{
while(x % i == 0)
{
x /= i;
primes[i] ++;
}
}
if(x > 1) primes[x]++;
}
LL res = 1;
for(auto prime:primes) res = res*(prime.second+1)%mod;
cout<<res<<endl;
}
约数之和
#include<bits/stdc++.h>
using namespace std;
const int mod = 1e9+7;
typedef long long LL;
int main()
{
int n;
cin>>n;
unordered_map<int,int>primes;
while(n--)
{
int x;
cin>>x;
for(int i = 2;i <= x/i;i++)
{
while(x % i == 0)
{
x /= i;
primes[i] ++;
}
}
if(x > 1) primes[x]++;
}
LL res = 1;
for(auto prime:primes)
{
LL t = 1;
int p = prime.first,a = prime.second;
while(a--) t = (t*p + 1) % mod;
res = res * t %mod;
}
cout<<res<<endl;
}
最大公约数
#include<bits/stdc++.h>
using namespace std;
int gcd(int a,int b)
{
return b?gcd(b,a%b):a;
}
int main()
{
int n;cin>>n;
while(n--)
{
int a,b;
cin>>a>>b;
cout<<gcd(a,b)<<endl;
}
return 0;
}
(3)欧拉函数
欧拉函数
#include<bits/stdc++.h>
using namespace std;
//欧拉函数的定义:1-N中与N互质的数的个数
int main()
{
int n;
cin>>n;
while(n--)
{
int x;
cin>>x;
int res = x;
for(int i = 2;i <= x/i;i++)
if(x % i == 0)
{
res = res/i*(i-1);//等价于res*(1-1/i),1/i可能是小数所以通分
while(x % i == 0) x /= i;
}
if(x > 1) res = res/x*(x-1);
cout<<res<<endl;
}
return 0;
}
筛法求欧拉函数
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N =1000100;
bool st[N];
int primes[N];
int phi[N];
int cnt;
LL get_ou(int n)
{
phi[1] = 1;
for(int i = 2;i <= n;i++)
{
if(!st[i])
{
primes[cnt++] = i;
phi[i] = i-1;//质数直接就是i-1个
}
for(int j = 0;primes[j] <= n/i;j++)
{
st[primes[j]*i] = true;
if(i % primes[j] == 0)
{
phi[primes[j]*i] = primes[j]*phi[i];
break;
}
phi[primes[j]*i] = phi[i]*(primes[j]-1);
}
}
LL res = 0;
for(int i = 1;i <= n;i++) res += phi[i];
return res;
}
int main()
{
int n;
cin>>n;
cout<<get_ou(n)<<endl;
return 0;
}
(4)快速幂
快速幂
给定 n组 ai,bi,pi,对于每组数据,求出 a(下标为i,上标为bi)mod pi值。
#include<bits/stdc++.h>
using namespace std;
#define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int n;
typedef long long LL;
LL qmi(int a,int k,int p)
{
LL res = 1;
while(k)
{
if(k & 1) res = (LL)res*a % p;
k >>= 1;
a = (LL)a*a % p;
}
return res;
}
int main()
{
IOS;
cin>>n;
while(n--)
{
int a,k,p;
cin>>a>>k>>p;
cout<<qmi(a,k,p)<<endl;
}
return 0;
}
快速幂求逆元
给定 n组 ai,pi,其中 pi是质数,求 ai 模 pi 的乘法逆元,若逆元不存在则输出
impossible
。注意:请返回在 0∼p−10∼p−1 之间的逆元。
乘法逆元的定义
若整数 b,m互质,并且对于任意的整数 a,如果满足 b|a,则存在一个整数 x,使得 a/b≡a×x(modm),则称 x 为 b 的模 m乘法逆元,记为 b−1(mod m)。
b 存在乘法逆元的充要条件是 b 与模数 m 互质。当模数 m 为质数时,bm−2 即为 b 的乘法逆元。
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
LL qmi(int a, int b, int p)
{
LL res = 1;
while (b)
{
if (b & 1) res = res * a % p;
a = a * (LL)a % p;
b >>= 1;
}
return res;
}
int main()
{
int n;
scanf("%d", &n);
while (n -- )
{
int a, p;
scanf("%d%d", &a, &p);
if (a % p == 0) puts("impossible");
else printf("%lld\n", qmi(a, p - 2, p));
}
return 0;
}
(5)扩展欧几里得算法
#include<bits/stdc++.h>
using namespace std;
int exgcd(int a,int b,int &x,int &y)
{
if(!b)
{
x = 1,y = 0;
return a;
}
int d = exgcd(b,a%b,y,x);
y -= a / b *x;
return d;
}
int main()
{
int n;
cin>>n;
while(n--)
{
int a,b,x,y;
cin>>a>>b;
exgcd(a,b,x,y);
cout<<x<<" "<<y<<endl;
}
return 0;
}
线性同余方程
#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
using namespace std;
typedef long long ll;
int exgcd(int a,int b,int &x,int &y)
{
if(!b)
{
x = 1,y = 0;
return a;
}
int d = exgcd(b,a%b,y,x);
y -= a / b *x;
return d;
}
int main()
{
IOS;
int n;
cin>>n;
while(n--)
{
int a,b,m,x,y;
cin>>a>>b>>m;
int d = exgcd(a,m,x,y);
//最后求的是x
if(b % d) puts("impossible");
else cout<< (ll)x*(b/d)%m <<endl;
}
return 0;
}
(6)中国剩余定理
表达整数的奇怪方式
给定 2n 个整数 a1,a2,…,an 和 m1,m2,…,mn,求一个最小的非负整数 x,满足 ∀i∈[1,n],x≡mi(mod ai)。
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
LL exgcd(LL a, LL b, LL &x, LL &y)
{
if (!b)
{
x = 1, y = 0;
return a;
}
LL d = exgcd(b, a % b, y, x);
y -= a / b * x;
return d;
}
int main()
{
int n;
cin >> n;
LL x = 0, m1, a1;
cin >> m1 >> a1;
for (int i = 0; i < n - 1; i ++ )
{
LL m2, a2;
cin >> m2 >> a2;
LL k1, k2;
LL d = exgcd(m1, m2, k1, k2);
if ((a2 - a1) % d)
{
x = -1;
break;
}
k1 *= (a2 - a1) / d;
k1 = (k1 % (m2/d) + m2/d) % (m2/d);
x = k1 * m1 + a1;
LL m = abs(m1 / d * m2);
a1 = k1 * m1 + a1;
m1 = m;
}
if (x != -1) x = (x % m1 + m1) % m1;
cout << x << endl;
return 0;
}
(7)高斯消元
高斯消元解线性方程组
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
const int N = 110;
const double eps = 1e-6;
int n;
double a[N][N];
int gauss()
{
int c, r;
for (c = 0, r = 0; c < n; c ++ )
{
int t = r;
for (int i = r; i < n; i ++ )
if (fabs(a[i][c]) > fabs(a[t][c]))
t = i;
if (fabs(a[t][c]) < eps) continue;
for (int i = c; i < n + 1; i ++ ) swap(a[t][i], a[r][i]);
for (int i = n; i >= c; i -- ) a[r][i] /= a[r][c];
for (int i = r + 1; i < n; i ++ )
if (fabs(a[i][c]) > eps)
for (int j = n; j >= c; j -- )
a[i][j] -= a[r][j] * a[i][c];
r ++ ;
}
if (r < n)
{
for (int i = r; i < n; i ++ )
if (fabs(a[i][n]) > eps)
return 2;
return 1;
}
for (int i = n - 1; i >= 0; i -- )
for (int j = i + 1; j < n; j ++ )
a[i][n] -= a[j][n] * a[i][j];
return 0;
}
int main()
{
cin >> n;
for (int i = 0; i < n; i ++ )
for (int j = 0; j < n + 1; j ++ )
cin >> a[i][j];
int t = gauss();
if (t == 0)
{
for (int i = 0; i < n; i ++ ) printf("%.2lf\n", a[i][n]);
}
else if (t == 1) puts("Infinite group solutions");
else puts("No solution");
return 0;
}
高斯消元解异或线性方程组
输入一个包含 nn 个方程 nn 个未知数的异或线性方程组。
方程组中的系数和常数为 00 或 11,每个未知数的取值也为 00 或 11。
求解这个方程组。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110;
int n;
int a[N][N];
int gauss()
{
int c, r;
for (c = 0, r = 0; c < n; c ++ )
{
int t = r;
for (int i = r; i < n; i ++ )
if (a[i][c])
t = i;
if (!a[t][c]) continue;
for (int i = c; i <= n; i ++ ) swap(a[r][i], a[t][i]);
for (int i = r + 1; i < n; i ++ )
if (a[i][c])
for (int j = n; j >= c; j -- )
a[i][j] ^= a[r][j];
r ++ ;
}
if (r < n)
{
for (int i = r; i < n; i ++ )
if (a[i][n])
return 2;
return 1;
}
for (int i = n - 1; i >= 0; i -- )
for (int j = i + 1; j < n; j ++ )
a[i][n] ^= a[i][j] * a[j][n];
return 0;
}
int main()
{
cin >> n;
for (int i = 0; i < n; i ++ )
for (int j = 0; j < n + 1; j ++ )
cin >> a[i][j];
int t = gauss();
if (t == 0)
{
for (int i = 0; i < n; i ++ ) cout << a[i][n] << endl;
}
else if (t == 1) puts("Multiple sets of solutions");
else puts("No solution");
return 0;
}
(8)求组合数
求组合数 I
给定 n 组询问,每组询问给定两个整数 a,b,请你输出Cabmod(10e9+7) 的值。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 2010, mod = 1e9 + 7;
int c[N][N];
void init()
{
for (int i = 0; i < N; i ++ )
for (int j = 0; j <= i; j ++ )
if (!j) c[i][j] = 1;
else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;
}
int main()
{
int n;
init();
scanf("%d", &n);
while (n -- )
{
int a, b;
scanf("%d%d", &a, &b);
printf("%d\n", c[a][b]);
}
return 0;
}
求组合数 II
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 100010, mod = 1e9 + 7;
int fact[N], infact[N];
int qmi(int a, int k, int p)
{
int res = 1;
while (k)
{
if (k & 1) res = (LL)res * a % p;
a = (LL)a * a % p;
k >>= 1;
}
return res;
}
int main()
{
fact[0] = infact[0] = 1;
for (int i = 1; i < N; i ++ )
{
fact[i] = (LL)fact[i - 1] * i % mod;
infact[i] = (LL)infact[i - 1] * qmi(i, mod - 2, mod) % mod;
}
int n;
scanf("%d", &n);
while (n -- )
{
int a, b;
scanf("%d%d", &a, &b);
printf("%d\n", (LL)fact[a] * infact[b] % mod * infact[a - b] % mod);
}
return 0;
}
求组合数 III
给定 n 组询问,每组询问给定三个整数 a,b,p,其中 p 是质数,请你输出 Cab mod p 的值。
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
int qmi(int a, int k, int p)
{
int res = 1;
while (k)
{
if (k & 1) res = (LL)res * a % p;
a = (LL)a * a % p;
k >>= 1;
}
return res;
}
int C(int a, int b, int p)
{
if (b > a) return 0;
int res = 1;
for (int i = 1, j = a; i <= b; i ++, j -- )
{
res = (LL)res * j % p;
res = (LL)res * qmi(i, p - 2, p) % p;
}
return res;
}
int lucas(LL a, LL b, int p)
{
if (a < p && b < p) return C(a, b, p);
return (LL)C(a % p, b % p, p) * lucas(a / p, b / p, p) % p;
}
int main()
{
int n;
cin >> n;
while (n -- )
{
LL a, b;
int p;
cin >> a >> b >> p;
cout << lucas(a, b, p) << endl;
}
return 0;
}
求组合数 IV
输入 a,b,求 Cab 的值。
注意结果可能很大,需要使用高精度计算。
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 5010;
int primes[N], cnt;
int sum[N];
bool st[N];
void get_primes(int n)
{
for (int i = 2; i <= n; i ++ )
{
if (!st[i]) primes[cnt ++ ] = i;
for (int j = 0; primes[j] <= n / i; j ++ )
{
st[primes[j] * i] = true;
if (i % primes[j] == 0) break;
}
}
}
int get(int n, int p)
{
int res = 0;
while (n)
{
res += n / p;
n /= p;
}
return res;
}
vector<int> mul(vector<int> a, int b)
{
vector<int> c;
int t = 0;
for (int i = 0; i < a.size(); i ++ )
{
t += a[i] * b;
c.push_back(t % 10);
t /= 10;
}
while (t)
{
c.push_back(t % 10);
t /= 10;
}
return c;
}
int main()
{
int a, b;
cin >> a >> b;
get_primes(a);
for (int i = 0; i < cnt; i ++ )
{
int p = primes[i];
sum[i] = get(a, p) - get(a - b, p) - get(b, p);
}
vector<int> res;
res.push_back(1);
for (int i = 0; i < cnt; i ++ )
for (int j = 0; j < sum[i]; j ++ )
res = mul(res, primes[i]);
for (int i = res.size() - 1; i >= 0; i -- ) printf("%d", res[i]);
puts("");
return 0;
}
满足条件的01序列
给定 n 个 0 和 n 个 1,它们将按照某种顺序排成长度为 2n 的序列,求它们能排列成的所有序列中,能够满足任意前缀序列中 0 的个数都不少于 1 的个数的序列有多少个。
输出的答案对 10e9+7 取模
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 100010, mod = 1e9 + 7;
int qmi(int a, int k, int p)
{
int res = 1;
while (k)
{
if (k & 1) res = (LL)res * a % p;
a = (LL)a * a % p;
k >>= 1;
}
return res;
}
int main()
{
int n;
cin >> n;
int a = n * 2, b = n;
int res = 1;
for (int i = a; i > a - b; i -- ) res = (LL)res * i % mod;
for (int i = 1; i <= b; i ++ ) res = (LL)res * qmi(i, mod - 2, mod) % mod;
res = (LL)res * qmi(n + 1, mod - 2, mod) % mod;
cout << res << endl;
return 0;
}
(9)容斥原理
能被整除的数
给定一个整数 n和 m 个不同的质数 p1,p2,…,pm。
请你求出 1∼n中能被 p1,p2,…,pm 中的至少一个数整除的整数有多少个。
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 20;
int p[N];
int main()
{
int n, m;
cin >> n >> m;
for (int i = 0; i < m; i ++ ) cin >> p[i];
int res = 0;
for (int i = 1; i < 1 << m; i ++ )
{
int t = 1, s = 0;
for (int j = 0; j < m; j ++ )
if (i >> j & 1)
{
if ((LL)t * p[j] > n)
{
t = -1;
break;
}
t *= p[j];
s ++ ;
}
if (t != -1)
{
if (s % 2) res += n / t;
else res -= n / t;
}
}
cout << res << endl;
return 0;
}
(10)博弈论
Nim游戏
给定 n堆石子,两位玩家轮流操作,每次操作可以从任意一堆石子中拿走任意数量的石子(可以拿完,但不能不拿),最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int main()
{
int n;
scanf("%d", &n);
int res = 0;
while (n -- )
{
int x;
scanf("%d", &x);
res ^= x;
}
if (res) puts("Yes");
else puts("No");
return 0;
}
台阶-Nim游戏
现在,有一个 nn 级台阶的楼梯,每级台阶上都有若干个石子,其中第 ii 级台阶上有 aiai 个石子(i≥1i≥1)。
两位玩家轮流操作,每次操作可以从任意一级台阶上拿若干个石子放到下一级台阶中(不能不拿)。
已经拿到地面上的石子不能再拿,最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int main()
{
int n;
scanf("%d", &n);
int res = 0;
for (int i = 1; i <= n; i ++ )
{
int x;
scanf("%d", &x);
if (i & 1) res ^= x;
}
if (res) puts("Yes");
else puts("No");
return 0;
}
集合-Nim游戏
给定 n 堆石子以及一个由 k 个不同正整数构成的数字集合 S。
现在有两位玩家轮流操作,每次操作可以从任意一堆石子中拿取石子,每次拿取的石子数量必须包含于集合 S,最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
#include <cstring>
#include <iostream>
#include <algorithm>
#include <unordered_set>
using namespace std;
const int N = 110, M = 10010;
int n, m;
int s[N], f[M];
int sg(int x)
{
if (f[x] != -1) return f[x];
unordered_set<int> S;
for (int i = 0; i < m; i ++ )
{
int sum = s[i];
if (x >= sum) S.insert(sg(x - sum));
}
for (int i = 0; ; i ++ )
if (!S.count(i))
return f[x] = i;
}
int main()
{
cin >> m;
for (int i = 0; i < m; i ++ ) cin >> s[i];
cin >> n;
memset(f, -1, sizeof f);
int res = 0;
for (int i = 0; i < n; i ++ )
{
int x;
cin >> x;
res ^= sg(x);
}
if (res) puts("Yes");
else puts("No");
return 0;
}
拆分-Nim游戏
给定 n 堆石子,两位玩家轮流操作,每次操作可以取走其中的一堆石子,然后放入两堆规模更小的石子(新堆规模可以为 0,且两个新堆的石子总数可以大于取走的那堆石子数),最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
#include <cstring>
#include <iostream>
#include <algorithm>
#include <unordered_set>
using namespace std;
const int N = 110;
int n;
int f[N];
int sg(int x)
{
if (f[x] != -1) return f[x];
unordered_set<int> S;
for (int i = 0; i < x; i ++ )
for (int j = 0; j <= i; j ++ )
S.insert(sg(i) ^ sg(j));
for (int i = 0;; i ++ )
if (!S.count(i))
return f[x] = i;
}
int main()
{
cin >> n;
memset(f, -1, sizeof f);
int res = 0;
while (n -- )
{
int x;
cin >> x;
res ^= sg(x);
}
if (res) puts("Yes");
else puts("No");
return 0;
}
六、动态规划
(1)背包问题
1.01背包问题
01背包,二维数组
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int f[N][N],v[N],w[N];
int n,m;
int main()
{
cin>>n>>m;
for(int i = 1;i <= n;i++) cin>>v[i]>>w[i];
for(int i = 1;i <= n;i++)
for(int j = 0;j <= m;j++)
{
f[i][j] = f[i-1][j];
if(j >= v[i])
f[i][j] = max(f[i][j],f[i-1][j-v[i]] + w[i]);
}
int res = 0;
//n件物品,容量为i的价值的最大值
for(int i = 0;i <= m;i++) res = max(res,f[n][i]);
cout<<res;
return 0;
}
一维数组优化
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int f[N],v[N],w[N];
int n,m;
int main()
{
cin>>n>>m;
for(int i = 1;i <= n;i++) cin>>v[i]>>w[i];
for(int i = 1;i <= n;i++)
for(int j = m;j >= v[i];j--)
f[j] = max(f[j],f[j-v[i]] + w[i]);//从后往前相当于都是以f[i-1][?]开始的
cout<<f[m]<<endl;
return 0;
}
2.完全背包问题
背包中数量无限多,与01背包题面上最大的区别
二维
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int v[N],w[N];
int dp[N][N];
int n,m;
//二维
int main()
{
cin>>n>>m;
for(int i = 1;i <= n;i++) cin>>v[i]>>w[i];
for(int i = 1;i <= n;i++)
for(int j = 0;j <= m;j++)
{
dp[i][j] = dp[i-1][j];
if(j >= v[i])
{
dp[i][j] = max(dp[i-1][j] ,dp[i][j-v[i]] + w[i]);
}
}
cout<<dp[n][m]<<endl;
return 0;
}
一维
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int v[N],w[N];
int n,m;
int dp[N];
//一维
int main()
{
cin>>n>>m;
for(int i = 1;i <= n;i++) cin>>v[i]>>w[i];
for(int i = 1;i <= n;i++)
for(int j = v[i];j <= m;j++)
dp[j] = max(dp[j],dp[j-v[i]]+w[i]);
cout<<dp[m]<<endl;
return 0;
}
3.多重背包问题
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int v[N],w[N],s[N];
int dp[N];
int n,m;
//二维 暴力
int main()
{
cin>>n>>m;
for(int i = 1;i <= n;i++) cin>>v[i]>>w[i]>>s[i];
for(int i = 1;i <= n;i++)
for(int j = m;j >= 0;j--)
{
for(int k = 0;k <= s[i]&&k*v[i] <= j;k++)
{
dp[j] = max(dp[j] ,dp[j-k*v[i]] + w[i]*k);
}
}
cout<<dp[m]<<endl;
return 0;
}
4.多重背包问题 II
#include<bits/stdc++.h>
using namespace std;
const int N = 2010;
int n,m;
int dp[N];
//二进制优化,按2的整次幂打包,对于每一个方案就是01背包问题
int v[N],w[N],s[N];
struct Good{
int v,w;
};
int main()
{
vector<Good>goods;
cin>>n>>m;
for(int i = 1;i <= n;i++) cin>>v[i]>>w[i]>>s[i];
for(int i = 1;i <= n;i ++)
{
for(int k = 1;k <= s[i];k *= 2)
{
s[i] -= k;
goods.push_back({k*v[i],k*w[i]});
}
if(s[i] > 0) goods.push_back({s[i]*v[i],s[i]*w[i]});
}
for(auto good : goods)
{
for(int j = m;j >= good.v;j--)
{
dp[j] = max(dp[j],dp[j-good.v] + good.w);
}
}
cout<<dp[m]<<endl;
return 0;
}
5.分组背包问题
#include<bits/stdc++.h>
using namespace std;
const int N = 110;
int f[N],v[N],w[N],s[N];
int n,m;
int main()
{
cin>>n>>m;
for(int i = 1;i <= n;i++)
{
int s;cin>>s;
for(int j = 0;j < s;j++) cin>>v[j]>>w[j];
for(int j = m;j >= 0;j--)
for(int k = 0;k < s;k++)
if(j >= v[k])
f[j] = max(f[j],f[j-v[k]]+w[k]);
}
cout<<f[m]<<endl;
return 0;
}
(2)线性DP
1.数字三角形
状态表示:由最后一层到当前点的权重最大值
#include<bits/stdc++.h>
using namespace std;
const int N = 510;
int w[N][N],f[N][N];
int n;
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n;
for(int i = 1;i <= n;i++)
for(int j = 1;j <= i;j++)
cin>>w[i][j];
//对f数组初始化
for(int i = 1;i <= n;i++) f[n][i] = w[n][i];
for(int i = n-1;i;i--)
for(int j = 1;j <= i;j++)
f[i][j] = max(f[i+1][j],f[i+1][j+1])+ w[i][j];
cout<<f[1][1]<<endl;
return 0;
}
2.最长上升子序列
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int a[N],f[N];
int n;
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n;
for(int i = 1;i <= n;i++)cin>>a[i];
for(int i = 1;i <= n;i++)
{
f[i] = 1;
for(int j = 1;j < i;j++)
{
if(a[i] > a[j])
{
f[i] = max(f[i],f[j]+1);
}
}
}
int res = 0;
for(int i = 1;i <= n;i++) res = max(res,f[i]);
cout<<res;
return 0;
}
3.最长上升子序列II
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int a[N],q[N];
int n;
int main()
{
cin>>n;
for(int i = 0;i < n;i++) cin>>a[i];
int len = 0;
for(int i = 0;i < n;i++)
{
int l = 0,r = len;
while(l < r)
{
int m = l + r + 1>> 1;
if(q[m] < a[i]) l = m;
else r = m-1;
}
len = max(len,r+1);
q[r+1] = a[i];//q[i]存的是长度为i的上升子序列的最后一个值的最小值
}
cout<<len<<endl;
return 0;
}
4.最长公共子序列
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int f[N][N];
char a[N],b[N];
int n,m;
int main()
{
cin>>n>>m>>a+1>>b+1;
for(int i = 1;i <= n;i++)
for(int j = 1;j <= m;j++)
{
f[i][j] = max(f[i-1][j],f[i][j-1]);
if(a[i] == b[j]) f[i][j] = max(f[i][j],f[i-1][j-1]+1);
}
cout<<f[n][m]<<endl;
return 0;
}
5.最短编辑距离
#include<bits/stdc++.h>
using namespace std;
#define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
const int N = 1010;
char a[N],b[N];
int n,m,f[N][N];
int main()
{
IOS;
cin>>n>>a+1>>m>>b+1;
//初始化
for(int i = 1;i <= m;i++) f[0][i] = i;
for(int i = 1;i <= n;i++) f[i][0] = i;
for(int i = 1;i <= n;i++)
for(int j = 1;j <= m;j++)
{
f[i][j] = min(f[i-1][j]+1,f[i][j-1]+1);
if(a[i]==b[j]) f[i][j] = min(f[i][j],f[i-1][j-1]);
else f[i][j] = min(f[i][j],f[i-1][j-1]+1);
}
cout<<f[n][m]<<endl;
return 0;
}
6.编辑距离
#include<bits/stdc++.h>
using namespace std;
const int N = 15,M = 1010;
int n,m;
int f[N][N];
char str[M][N];
int edit(char a[],char b[])
{
int la = strlen(a+1),lb = strlen(b+1);
for(int i = 0;i <= la;i++) f[i][0] = i;
for(int i = 0;i <= lb;i++) f[0][i] = i;
for(int i = 1;i <= la;i++)
for(int j = 1;j <= lb;j++)
{
f[i][j] = min(f[i-1][j]+1,f[i][j-1]+1);
f[i][j] = min(f[i][j],f[i-1][j-1]+(a[i] != b[j]));
}
return f[la][lb];
}
int main()
{
cin>>n>>m;
for(int i = 0;i < n;i++) cin>>(str[i]+1);
while(m--)
{
int lim;
char s[N];
cin>>s+1>>lim;
int res = 0;
for(int i = 0;i < n;i++)
{
if(edit(str[i],s) <= lim)
res++;
}
cout<<res<<endl;
}
return 0;
}
(3)区间DP
石子合并
设有 N堆石子排成一排,其编号为 1,2,3,…,N。
每堆石子有一定的质量,可以用一个整数来描述,现在要将这 N 堆石子合并成为一堆。
每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。
#include<bits/stdc++.h>
using namespace std;
const int N = 310;
int f[N][N];
int s[N];
int n;
int main()
{
cin>>n;
for(int i = 1;i <= n;i++) cin>>s[i],s[i] += s[i-1];
for(int len = 2;len <= n;len++)
{
for(int i = 1;i + len - 1 <= n;i++)
{
int j = i + len - 1;
f[i][j] = 2e8;
for(int k = i;k < j;k++)
{
f[i][j] = min(f[i][j],f[i][k]+f[k+1][j]+s[j]-s[i-1]);
}
}
}
cout<<f[1][n]<<endl;
return 0;
}
(4)计数类DP
整数划分
一个正整数 n 可以表示成若干个正整数之和,形如:n=n1+n2+…+nk,其中 n1≥n2≥…≥nk,k≥1。
我们将这样的一种表示称为正整数 n的一种划分。
现在给定一个正整数 n,请你求出 n 共有多少种不同的划分方法。
完全背包问题的思路
#include<bits/stdc++.h>
using namespace std;
const int N = 1010,mod = 1e9+7;
int f[N];
int n;
//完全背包的思路
//f[i][j]表示只从1~i中选,且总和等于j的方案数
//状态转移方程:f[i][j] = f[i-1][j]+f[i][j-i]
int main()
{
cin>>n;
f[0] = 1;
for(int i = 1;i <= n;i++)
for(int j = i;j <= n;j++)
f[j] = (f[j]+f[j-i])%mod;
cout<<f[n]<<endl;
return 0;
}
特殊的思路
#include<bits/stdc++.h>
using namespace std;
const int N = 1010,mod = 1e9+7;
int f[N][N];
int n;
//f[i][j]指和为i,有j种方案数
//f[i][j] = f[i-1][j-1]+f[i-j][j];
int main()
{
cin>>n;
f[0][0] = 1;
for(int i = 1;i <= n;i++)
for(int j = 1;j <= i;j++)
f[i][j] = (f[i-1][j-1]+f[i-j][j])%mod;
int res = 0;
//所有方案数求和
for(int i = 1;i <= n;i++) res = (res + f[n][i])%mod;
cout<<res;
return 0;
}
(5)数位统计DP
计数问题
给定两个整数 a 和 b,求 a 和 b 之间的所有数字中 0∼9 的出现次数。
例如,a=1024,b=1032,则 a 和 b 之间共有 9 个数如下:
1024 1025 1026 1027 1028 1029 1030 1031 1032
其中
0
出现 10 次,1
出现 10 次,2
出现 7 次,3
出现 3 次等等…
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 10;
/*
001~abc-1, 999
abc
1. num[i] < x, 0
2. num[i] == x, 0~efg
3. num[i] > x, 0~999
*/
int get(vector<int> num, int l, int r)
{
int res = 0;
for (int i = l; i >= r; i -- ) res = res * 10 + num[i];
return res;
}
int power10(int x)
{
int res = 1;
while (x -- ) res *= 10;
return res;
}
int count(int n, int x)
{
if (!n) return 0;
vector<int> num;
while (n)
{
num.push_back(n % 10);
n /= 10;
}
n = num.size();
int res = 0;
for (int i = n - 1 - !x; i >= 0; i -- )
{
if (i < n - 1)
{
res += get(num, n - 1, i + 1) * power10(i);
if (!x) res -= power10(i);
}
if (num[i] == x) res += get(num, i - 1, 0) + 1;
else if (num[i] > x) res += power10(i);
}
return res;
}
int main()
{
int a, b;
while (cin >> a >> b , a)
{
if (a > b) swap(a, b);
for (int i = 0; i <= 9; i ++ )
cout << count(b, i) - count(a - 1, i) << ' ';
cout << endl;
}
return 0;
}
(6)状态压缩DP
蒙德里安的梦想
求把 N×M 的棋盘分割成若干个 1×2 的的长方形,有多少种方案。
例如当 N=2,M=4 时,共有 5 种方案。当 N=2,M=3 时,共有 3 种方案。
朴素写法,1000ms
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 12, M = 1 << N;
int n, m;
long long f[N][M];
bool st[M];
int main()
{
while (cin >> n >> m, n || m)
{
for (int i = 0; i < 1 << n; i ++ )
{
int cnt = 0;
st[i] = true;
for (int j = 0; j < n; j ++ )
if (i >> j & 1)
{
if (cnt & 1) st[i] = false;
cnt = 0;
}
else cnt ++ ;
if (cnt & 1) st[i] = false;
}
memset(f, 0, sizeof f);
f[0][0] = 1;
for (int i = 1; i <= m; i ++ )
for (int j = 0; j < 1 << n; j ++ )
for (int k = 0; k < 1 << n; k ++ )
if ((j & k) == 0 && st[j | k])
f[i][j] += f[i - 1][k];
cout << f[m][0] << endl;
}
return 0;
}
去除无效状态的优化写法,230ms
#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
typedef long long LL;
const int N = 12, M = 1 << N;
int n, m;
LL f[N][M];
vector<int> state[M];
bool st[M];
int main()
{
while (cin >> n >> m, n || m)
{
for (int i = 0; i < 1 << n; i ++ )
{
int cnt = 0;
bool is_valid = true;
for (int j = 0; j < n; j ++ )
if (i >> j & 1)
{
if (cnt & 1)
{
is_valid = false;
break;
}
cnt = 0;
}
else cnt ++ ;
if (cnt & 1) is_valid = false;
st[i] = is_valid;
}
for (int i = 0; i < 1 << n; i ++ )
{
state[i].clear();
for (int j = 0; j < 1 << n; j ++ )
if ((i & j) == 0 && st[i | j])
state[i].push_back(j);
}
memset(f, 0, sizeof f);
f[0][0] = 1;
for (int i = 1; i <= m; i ++ )
for (int j = 0; j < 1 << n; j ++ )
for (auto k : state[j])
f[i][j] += f[i - 1][k];
cout << f[m][0] << endl;
}
return 0;
}
最短Hamilton路径
给定一张 n 个点的带权无向图,点从 0∼n−1标号,求起点 0 到终点 n−1的最短 Hamilton 路径。
Hamilton 路径的定义是从 0 到 n−1 不重不漏地经过每个点恰好一次。
#include<bits/stdc++.h>
using namespace std;
const int N = 20,M = 1 << N;
int w[N][N],f[M][N];
int n;
int main()
{
cin>>n;
for(int i = 0;i < n;i++)
for(int j = 0;j < n;j++)
cin>>w[i][j];
memset(f,0x3f,sizeof f);
f[1][0] = 0;//初始化,起点为0
for(int i = 0;i < 1 << n;i++)
for(int j = 0;j < n;j++)
if(i >> j & 1)
{
for(int k = 0;k < n;k++)
{
if(i >> k&1)
{
f[i][j] = min(f[i][j],f[i-(1<<j)][k]+w[k][j]);
}
}
}
cout<<f[(1<<n)-1][n-1]<<endl;
return 0;
}
(7)树形DP
没有上司的舞会
Ural 大学有 N 名职员,编号为 1∼N。
他们的关系就像一棵以校长为根的树,父节点就是子节点的直接上司。
每个职员有一个快乐指数,用整数 Hi 给出,其中 1≤i≤N。
现在要召开一场周年庆宴会,不过,没有职员愿意和直接上司一起参会。
在满足这个条件的前提下,主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求这个最大值。
#include<bits/stdc++.h>
using namespace std;
const int N = 10010;
int happy[N],f[N][2];
int e[N],ne[N],h[N],idx;
bool has_father[N];
int n;
void add(int a,int b)
{
e[idx] = b;ne[idx] = h[a];h[a] = idx++;
}
void dfs(int u)
{
f[u][1] = happy[u];
for(int i = h[u];i != -1;i = ne[i])
{
int j = e[i];
dfs(j);//找到每一个树的叶节点,然后回溯求值
f[u][0] += max(f[j][0],f[j][1]);
f[u][1] += f[j][0];
}
}
int main()
{
cin>>n;
for(int i = 1;i <= n;i++) cin>>happy[i];
memset(h,-1,sizeof h);
for(int i = 0;i < n-1;i++)
{
int a,b;
cin>>a>>b;//b是a 的父节点
has_father[a] = true;
add(b,a);
}
int root = 1;
while(has_father[root]) root++;
dfs(root);
//f[root][0]不选根节点,f[root][1]选根节点
cout<<max(f[root][0],f[root][1])<<endl;
return 0;
}
(8)记忆化搜索
滑雪
给定一个 RR 行 CC 列的矩阵,表示一个矩形网格滑雪场。
矩阵中第 ii 行第 jj 列的点表示滑雪场的第 ii 行第 jj 列区域的高度。
一个人从滑雪场中的某个区域内出发,每次可以向上下左右任意一个方向滑动一个单位距离。
当然,一个人能够滑动到某相邻区域的前提是该区域的高度低于自己目前所在区域的高度。
下面给出一个矩阵作为例子:
1 2 3 4 5 16 17 18 19 6 15 24 25 20 7 14 23 22 21 8 13 12 11 10 9
在给定矩阵中,一条可行的滑行轨迹为 24−17−2−1。
在给定矩阵中,最长的滑行轨迹为 25−24−23−…−3−2−1,沿途共经过 25 个区域。
现在给定你一个二维矩阵表示滑雪场各区域的高度,请你找出在该滑雪场中能够完成的最长滑雪轨迹,并输出其长度(可经过最大区域数)。
#include<bits/stdc++.h>
using namespace std;
const int N = 310;
int h[N][N];
int f[N][N];
int n,m;
int dx[4] = {-1,0,1,0},dy[4] = {0,1,0,-1};
int dp(int x,int y)
{
int &v = f[x][y];//取地址
if(v != -1) return v;
v = 1;
for(int i = 0;i < 4;i++)
{
int a = x + dx[i],b = y + dy[i];
if(a >= 1&&a <= n&&b >= 1&&b <= m&&h[x][y] > h[a][b])
v = max(v,dp(a,b)+1);
}
return v;
}
int main()
{
cin>>n>>m;
for(int i = 1;i <= n;i++)
for(int j = 1; j <= m;j++)
cin>>h[i][j];
memset(f,-1,sizeof f);
int res = 0;
for(int i = 1;i <= n;i++)
for(int j = 1;j <= m;j++)
res = max(res,dp(i,j));
cout<<res;
return 0;
}
七、 贪心
(1)区间问题
区间选点
给定 N 个闭区间 [ai,bi],请你在数轴上选择尽量少的点,使得每个区间内至少包含一个选出的点。
输出选择的点的最小数量。
位于区间端点上的点也算作区间内。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
struct Range
{
int l, r;
bool operator< (const Range &W)const
{
return r < W.r;
}
}range[N];
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i ++ ) scanf("%d%d", &range[i].l, &range[i].r);
sort(range, range + n);
int res = 0, ed = -2e9;
for (int i = 0; i < n; i ++ )
if (range[i].l > ed)
{
res ++ ;
ed = range[i].r;
}
printf("%d\n", res);
return 0;
}
最大不相交区间数量
给定 N 个闭区间[ai,bi],请你在数轴上选择若干区间,使得选中的区间之间互不相交(包括端点)。
输出可选取区间的最大数量。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
struct Range
{
int l, r;
bool operator< (const Range &W)const
{
return r < W.r;
}
}range[N];
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i ++ ) scanf("%d%d", &range[i].l, &range[i].r);
sort(range, range + n);
int res = 0, ed = -2e9;
for (int i = 0; i < n; i ++ )
if (ed < range[i].l)
{
res ++ ;
ed = range[i].r;
}
printf("%d\n", res);
return 0;
}
区间分组
给定 N个闭区间 [ai,bi],请你将这些区间分成若干组,使得每组内部的区间两两之间(包括端点)没有交集,并使得组数尽可能小。输出最小组数。
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 100010;
int n;
struct Range
{
int l, r;
bool operator< (const Range &W)const
{
return l < W.l;
}
}range[N];
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i ++ )
{
int l, r;
scanf("%d%d", &l, &r);
range[i] = {l, r};
}
sort(range, range + n);
priority_queue<int, vector<int>, greater<int>> heap;
for (int i = 0; i < n; i ++ )
{
auto r = range[i];
if (heap.empty() || heap.top() >= r.l) heap.push(r.r);
else
{
heap.pop();
heap.push(r.r);
}
}
printf("%d\n", heap.size());
return 0;
}
区间覆盖
给定 N 个闭区间 [ai,bi] 以及一个线段区间 [s,t],请你选择尽量少的区间,将指定线段区间完全覆盖。
输出最少区间数,如果无法完全覆盖则输出 −1−1。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
struct Range
{
int l, r;
bool operator< (const Range &W)const
{
return l < W.l;
}
}range[N];
int main()
{
int st, ed;
scanf("%d%d", &st, &ed);
scanf("%d", &n);
for (int i = 0; i < n; i ++ )
{
int l, r;
scanf("%d%d", &l, &r);
range[i] = {l, r};
}
sort(range, range + n);
int res = 0;
bool success = false;
for (int i = 0; i < n; i ++ )
{
int j = i, r = -2e9;
while (j < n && range[j].l <= st)
{
r = max(r, range[j].r);
j ++ ;
}
if (r < st)
{
res = -1;
break;
}
res ++ ;
if (r >= ed)
{
success = true;
break;
}
st = r;
i = j - 1;
}
if (!success) res = -1;
printf("%d\n", res);
return 0;
}
(2)Huffman树
合并果子
在一个果园里,达达已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。
达达决定把所有的果子合成一堆。
每一次合并,达达可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。
可以看出,所有的果子经过 n−1n−1 次合并之后,就只剩下一堆了。
达达在合并果子时总共消耗的体力等于每次合并所耗体力之和。
因为还要花大力气把这些果子搬回家,所以达达在合并果子时要尽可能地节省体力。
假定每个果子重量都为 11,并且已知果子的种类数和每种果子的数目,你的任务是设计出合并的次序方案,使达达耗费的体力最少,并输出这个最小的体力耗费值。
例如有 33 种果子,数目依次为 1,2,91,2,9。
可以先将 1、21、2 堆合并,新堆数目为 33,耗费体力为 33。
接着,将新堆与原先的第三堆合并,又得到新的堆,数目为 1212,耗费体力为 1212。
所以达达总共耗费体力=3+12=15=3+12=15。
可以证明 1515 为最小的体力耗费值。
每次都挑最小的两堆,小根堆优先队列实现
#include<bits/stdc++.h>
using namespace std;
int n;
const int N = 10000+50;
priority_queue<int,vector<int>,greater<int>>f;//小根堆从小到大
int main()
{
//freopen("P1090_2.in","r",stdin);
//freopen("P1090_2.out","w",stdout);
cin>>n;
for(int i = 1;i <= n;i++)
{
int x;cin>>x;
f.push(x);
}
long long res = 0;
while(f.size() != 1)
{
int t1 = f.top();
f.pop();
int t2 = f.top();
f.pop();
res += t1 + t2;
f.push(t1+t2);
}
cout<<res<<endl;
return 0;
}
(3)排序不等式
有 n 个人排队到 1 个水龙头处打水,第 i 个人装满水桶所需的时间是 ti,请问如何安排他们的打水顺序才能使所有人的等待时间之和最小?
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 100010;
int n;
int t[N];
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i ++ ) scanf("%d", &t[i]);
sort(t, t + n);
reverse(t, t + n);
LL res = 0;
for (int i = 0; i < n; i ++ ) res += t[i] * i;
printf("%lld\n", res);
return 0;
}
(4)绝对值不等式
货仓选址
在一条数轴上有 N 家商店,它们的坐标分别为 A1∼AN。
现在需要在数轴上建立一家货仓,每天清晨,从货仓到每家商店都要运送一车商品。
为了提高效率,求把货仓建在何处,可以使得货仓到每家商店的距离之和最小。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
int q[N];
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);
sort(q, q + n);
int res = 0;
for (int i = 0; i < n; i ++ ) res += abs(q[i] - q[n / 2]);
printf("%d\n", res);
return 0;
}
(5)推公式
耍杂技的牛
农民约翰的 N 头奶牛(编号为 1…N)计划逃跑并加入马戏团,为此它们决定练习表演杂技。
奶牛们不是非常有创意,只提出了一个杂技表演:
叠罗汉,表演时,奶牛们站在彼此的身上,形成一个高高的垂直堆叠。
奶牛们正在试图找到自己在这个堆叠中应该所处的位置顺序。
这 NN 头奶牛中的每一头都有着自己的重量 Wi 以及自己的强壮程度 Si。
一头牛支撑不住的可能性取决于它头上所有牛的总重量(不包括它自己)减去它的身体强壮程度的值,现在称该数值为风险值,风险值越大,这只牛撑不住的可能性越高。
您的任务是确定奶牛的排序,使得所有奶牛的风险值中的最大值尽可能的小。
#include <iostream>
#include <algorithm>
using namespace std;
typedef pair<int, int> PII;
const int N = 50010;
int n;
PII cow[N];
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i ++ )
{
int s, w;
scanf("%d%d", &w, &s);
cow[i] = {w + s, w};
}
sort(cow, cow + n);
int res = -2e9, sum = 0;
for (int i = 0; i < n; i ++ )
{
int s = cow[i].first - cow[i].second, w = cow[i].second;
res = max(res, sum - s);
sum += w;
}
printf("%d\n", res);
return 0;
}
八、STL
(1)vector容器
vector容器能存放任何类型的对象
功能 | 例子 | 说明 |
---|---|---|
赋值 | a.push_back(100); | 在尾部添加元素 |
元素个数 | int size = a.size(); | 元素个数 |
是否为空 | bool isEmpty = a.empty(); | 判断是否为空 |
打印 | cout<<a[0]<<endl; | 打印第一个元素 |
中间插入 | a.insert(a.begin()+i, k); | 在第i个元素前面插入k |
尾部插入 | a.push_back(8); | 尾部插入值为8的元素 |
尾部插入 | a.insert(a.end(), 10,5); | 尾部插入10个值为5的元素 |
删除尾部 | a.pop_back(); | 删除末尾元素 |
删除区间 | a.erase(a.begin()+i, a.begin()+j); | 删除区间[i, j-1]的元素 |
删除元素 | a.erase(a.begin()+2); | 删除第3个元素(线性时间) |
调整大小 | a.resize(n) | 数组大小变为n |
清空 | a.clear(); | |
翻转 | reverse(a.begin(), a.end()); | 用函数reverse翻转数组 |
排序 | sort(a.begin(), a.end()); | 用函数sort排序,从小到大 |
(2)队列,优先队列
主要操作
例子 | 说明 |
---|---|
queue q; | 定义栈,Type为数据类型,如int,float,char等 |
q. push(item); | 把item放进队列 |
q.front(); | 返回队首元素,但不会删除 |
q.pop(); | 删除队首元素 |
q.back(); | 返回队尾元素 |
q.size(); | 返回元素个数 |
q.empty(); | 检查队列是否为空 |
(3)栈
主要操作
例子 | 说明 |
---|---|
stack< Type > stk; | 定义栈,Type为数据类型,如int,float,char等。 |
stk.push(item); | 把item放到栈顶。 |
stk.top(); | 返回栈顶的元素,但不会删除。 |
stk.pop(); | 删除栈顶的元素,但不会返回。 |
stk.size(); | 返回栈中元素的个数。 |
stk.empty(); | 检查栈是否为空,如果为空返回true,否则返回false。 |
(4)map容器
•map:关联容器,实现从键(key)到值(value)的映射。
•map效率高的原因:用平衡二叉搜索树来存储和访问。
•还存在unordered_map,插入和查找效率更高
(5)优先队列和priority_queue
•优先队列:优先级最高的先出队。
•队列和排序的完美结合,不仅可以存储数据,还可以将这些数据按照设定的规则进行排序。
•每次的push和pop操作,优先队列都会动态调整,把优先级最高的元素放在前面。
•优先队列一般可以替代堆
(6)set
•set:集合,集合中的元素是唯一的(去重)。
•STL的set用二叉搜索树实现,集合中的每个元素只出现一次,且是排好序的。访问元素的时间复杂度是O(logn)的。
•其次,也存在muti_set,unordered_set,都非常实用,可以自行了解
•set和map在竞赛题中应用很广泛。特别是需要用二叉搜索树处理数据的题目,如果用set或map实现,能极大地简化代码。
主要操作
例子 | 说明 |
---|---|
set A; | 定义 |
A. insert(item); | 把item放进set |
A.erase(item); | 删除元素item |
A.clear(); | 清空set |
A.empty (); | 判断是否为空 |
A.size(); | 返回元素个数 |
A.find(k); | 返回一个迭代器,指向键值k |
A.lower_bound(k); | 返回一个迭代器,指向键值不小于k的第一个元素 |
A.upper_bound(); | 返回一个迭代器,指向键值大于k的第一个元素 |
(7)next_permutation
•next_permutation(): 求“下一个”排列组合。
•例如三个字符{a, b, c}组成的序列,next_permutation()能按字典序返回6个组合:abc,acb,bac,bca,cab,cba。
•函数next_permutation()的定义有两种形式:
–bool next_permutation (BidirectionalIterator first, BidirectionalIterator last);
–bool next_permutation (BidirectionalIterator first, BidirectionalIterator last, Compare comp);
•返回值:如果没有下一个排列组合,返回false,否则返回true。每执行next_permutation()一次,会把新的排列放到原来的空间里。
•复杂度:O(n)。
•排列的范围:[first, last),包括first,不包括last。
#include<bits/stdc++.h>
using namespace std;
string s;
int main(){
cin>>s;
do
{
cout<<s<<"\n";
}
while(next_permutation(s.begin(),s.end()));
return 0;
}
九、由数据范围反推算法复杂度以及算法内容
一般ACM或者笔试题的时间限制是1秒或2秒。
在这种情况下,C++代码中的操作次数控制在 107∼108107∼108 为最佳。
下面给出在不同数据范围下,代码的时间复杂度和算法该如何选择:
n≤30n≤30, 指数级别, dfs+剪枝,状态压缩dp
n≤100n≤100 => O(n3)O(n3),floyd,dp,高斯消元
n≤1000n≤1000 => O(n2)O(n2),O(n2logn)O(n2logn),dp,二分,朴素版Dijkstra、朴素版Prim、Bellman-Ford
n≤10000n≤10000 => O(n∗n√)O(n∗n),块状链表、分块、莫队
n≤100000n≤100000 => O(nlogn)O(nlogn) => 各种sort,线段树、树状数组、set/map、heap、拓扑排序、dijkstra+heap、prim+heap、spfa、求凸包、求半平面交、二分、CDQ分治、整体二分
n≤1000000n≤1000000 => O(n)O(n), 以及常数较小的 O(nlogn)O(nlogn) 算法 => 单调队列、 hash、双指针扫描、并查集,kmp、AC自动机,常数比较小的 O(nlogn)O(nlogn) 的做法:sort、树状数组、heap、dijkstra、spfa
n≤10000000n≤10000000 => O(n)O(n),双指针扫描、kmp、AC自动机、线性筛素数
n≤109n≤109 => O(n√)O(n),判断质数
n≤1018n≤1018 => O(logn)O(logn),最大公约数,快速幂
n≤101000n≤101000 => O((logn)2)O((logn)2),高精度加减乘除
n≤10100000n≤10100000 => O(logk×loglogk),k表示位数O(logk×loglogk),k表示位数,高精度加减、FFT/NTT