(一)第一章-基础算法
一.快速排序
1.基本操作
O(nlogn)
#include <iostream>
using namespace std;
const int N=1e6+10;
int n;
int q[N];
void QuickSort(int q[],int l,int r){
int i=l-1,j=r+1;//i和j在两侧
int mid=l+r>>1;//中间结点作分界点
int x=q[mid];
if(l>=r){//只有一个元素,直接返回
return;
}
while(i<j){
do i++ ; while (q[i]<x);
do j-- ; while (q[j]>x);
if(i<j) swap(q[i],q[j]);
}
QuickSort(q,l,j);
QuickSort(q,j+1,r);
}
int main(){
scanf("%d",&n);
for(int i=0;i<n;i++){
scanf("%d",&q[i]);
}
QuickSort(q,0,n-1);
for(int i=0;i<n;i++){
printf("%d ",q[i]);
}
return 0;
}
2.第k个数
O(n)
#include <iostream>
using namespace std;
const int N = 100010;
int n,k;
int q[N];
int QuickSort(int l,int r,int k){
int i = l - 1,j = r + 1;
int mid = l + r >>1;
int x = q[mid];
// 如果找到了则直接返回
if(l >= r) return q[l];
while (i < j){
do(i++); while (q[i] < x);
do(j--); while (q[j] > x);
if(i < j) swap(q[i],q[j]);
}
// 如果第 k 大的数在左半边,则递归左半边寻找
if(k <= j){
return QuickSort(l,j,k);
} else {// 否则递归右半边
return QuickSort(j + 1, r,k);
}
}
int main(){
cin >> n >> k;
for(int i=0;i<n;i++) cin >> q[i];
// 返回的就是第 k 大的数,注意这里传入的是下标 k - 1
int res = QuickSort(0,n-1,k-1);
cout << res << endl;
return 0;
}
二.归并排序
1.基本操作
#include <iostream>
using namespace std;
const int N=1e6+10;
int n;
int q[N],tmp[N];
void MergeSort(int q[],int l,int r){
int mid=l+r>>1;
int i=l,j=mid+1,k=0;
if(l>=r) return;
MergeSort(q,l,mid);//前半段递归
MergeSort(q,mid+1,r);//后半段递归
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(){
scanf("%d",&n);
for(int i=0;i<n;i++){
scanf("%d",&q[i]);
}
MergeSort(q,0,n-1);
for(int i=0;i<n;i++){
printf("%d ",q[i]);
}
return 0;
}
2. 逆序对的数量
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 100010;
int n;
int q[N],tmp[N];
LL res = 0;
void MergeSort(int l,int r){
int mid = l + r >> 1;
int i = l,j = mid + 1,k = 0;
if(l >= r) return;
MergeSort(l,mid);
MergeSort(mid + 1,r);
while (i <= mid && j <= r){
if(q[i] <= q[j]) tmp[k++] = q[i++];
else {
tmp[k++] = q[j++];
// 在 q[i] > q[j] 下,q[i]-mid 的所有数 > q[i] > q[j],
// 逆序对数量即为 mid - i + 1
res += mid - i + 1;
}
}
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(){
cin >> n;
for(int i=0;i<n;i++) cin >> q[i];
MergeSort(0,n-1);
cout << res << endl;
return 0;
}
三.二分
1.数的范围
#include <iostream>
using namespace std;
const int N=100010;
int n,m;
int q[N];
int main(){
cin >> n >> m;
for(int i=0;i<n;i++) scanf("%d",&q[i]);
while(m--){
int x;
cin >> x;
int l=0,r=n-1;
while (l<r){
int mid=(l+r)>>1;
// 寻找左边界
if(q[mid]>=x) r=mid;
else l=mid+1;
if(q[l]!=x){//与左边界元素不相等
printf("-1 -1\n");
} else{
printf("%d ",l);
int l=0,r=n-1;
while (l<r){
int mid=(l+r+1)>>1;//注意加1
// 寻找右边界
if(q[mid]<=x) l=mid;
else r=mid-1;
}
printf("%d\n",l);
}
}
return 0;
}
2.数的三次方根
#include <iostream>
using namespace std;
double n;
int main(){
scanf("%lf",&n);
double l=-100,r=100,mid;
while (r-l>1e-8){//循环条件,不需要注意边界
mid=(l+r)/2;
if(mid*mid*mid<=n){
l=mid;
} else{
r=mid;
}
}
printf("%.6lf",l);
return 0;
}
四.高精度
1.高精度加法
#include <iostream>
#include <vector>
using namespace std;
// C = A+B;
vector<int> add(vector<int> &A,vector<int> &B){
vector<int> C;
int t=0;
for(int i=0;i<A.size() || i<B.size();i++){
if(i<A.size()) t+=A[i];// 加上A的第i位上的数字
if(i<B.size()) t+=B[i];// 加上B的第i位上的数字
C.push_back(t % 10);
t /= 10;// 计算是否有进位
}
if(t) C.push_back(1);// 最高位还有进位
return C;
}
int main(){
string a,b;// 以字符串形式保存输入的两个整数
vector<int> A,B;
cin >> a >> b;// a = "123456"
for(int i=a.size()-1;i>=0;i--) A.push_back(a[i]-'0');// A = [6,5,4,3,2,1]
for(int i=b.size()-1;i>=0;i--) A.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;
}
2.高精度减法
#include <iostream>
#include <vector>
using namespace std;
// 判断是否有A>=B
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;
}
// C = A-B;
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;// t为借位,t=A[i]-B[i]-t,B[i]需要判断有没有
if(i<B.size()) t -= B[i];
// 这里如果没有借位,(t + 10) % 10就刚好等于t
// 如果这里有借位,(t + 10) % 10就会借一个10下来
C.push_back((t+10)%10);
// 如果t < 0,说明不够减,需要借位,把t赋值为1,就是在下一次执行中,A的当前位会减掉t
if(t<0) t=1;
else t=0;
}
// 删除前面的0,也保留了可能为答案的0
while(C.size()>1 && C.back()==0) C.pop_back();
return C;
}
int main(){
string a,b;
vector<int> A,B,C;
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');
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;
}
3.高精度乘法
#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();i++){
t += A[i]*b;// t + A[i] * b = 7218
C.push_back(t%10);// 只取个位 8
t /= 10;// 721 看作进位
}
// 处理最后剩余的t
while(t){
C.push_back(t%10);
t /= 10;
}
// 删除前面的0,也保留了可能为答案的0
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--) cout << C[i];
cout << endl;
return 0;
}
4.高精度除法
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// A/b,商是C,余数是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];//将上次的余数*10在加上当前位的数字,便是该位需要除的被除数
C.push_back(r/b);
r %= b;// 得到余数
}
//由于在除法运算中,高位到低位运算,因此C的前导零都在vector的前面而不是尾部,vector只有删除最后一个数字pop_back是常数复杂度,而对于删除第一位没有相应的库函数可以使用,而且删除第一位,其余位也要前移,
//因此我们将C翻转,这样0就位于数组尾部,可以使用pop函数删除前导0
reverse(C.begin(),C.end());
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');
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;
}
五.前缀和与差分
1.前缀和
#include <iostream>
#include <algorithm>
using namespace std;
const int N=100010;
int n,m;
int a[N],s[N];// 这里s[0]=0可以防止后面出现越界
int main(){
cin >> n >> m;
for(int i=1;i<=n;i++) cin >> a[i];
// 计算前缀和
for(int i=1;i<=n;i++) s[i]=s[i-1]+a[i];
while(m--){
int l,r;
cin >> l >> r;
cout << s[r]-s[l-1] << endl;// 区间
}
return 0;
}
2.子矩阵的和
#include <iostream>
using namespace std;
const int N=1010;
int n,m,q;
int a[N][N],s[N][N];
int main(){
cin >> n >> m >> q;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin >> a[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;
cin >> x1 >> y1 >> x2 >> y2;
// 计算面积元素和
cout << s[x2][y2] - s[x1-1][y2] - s[x2][y1-1] + s[x1-1][y1-1];
}
return 0;
}
3.差分
#include <iostream>
using namespace std;
const int N=100010;
int n,m;
int a[N],b[N];
// 差分数组:a[i] = b[1]+b[2]+……+b[i]
// b[l] += c;只影响 b[l] 之后的数据
int main(){
cin >> n >> m;
for(int i=1;i<=n;i++) {
cin >> a[i];
// 构建差分数组
b[i] = a[i]-a[i-1];
}
while (m--){
int l,r,c;
cin >> l >> r >> c;
// 将a序列中[l, r]之间的每个数都加上c
b[l] += c;// l之后加c
b[r+1] -= c;// r之后减c
}
for(int i=1;i<=n;i++){
// 前缀和运算,为构建差分数组逆运算
a[i] = b[i]+a[i-1];
cout << a[i];
}
return 0;
}
4.差分矩阵
#include <iostream>
using namespace std;
const int N=1010;
int n,m,q;
int a[N][N],b[N][N];
int main(){
cin >> n >> m >> q;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin >> a[i][j];
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
// 构建差分矩阵
b[i][j] = a[i][j] - a[i-1][j] - a[i][j-1] + a[i-1][j-1];
}
}
while(q--){
int x1,y1,x2,y2,c;
cin >> x1 >> y1 >> x2 >> y2 >> c;
//对b数组执行插入操作,等价于对a数组中的(x1,y1)到(x2,y2)之间的元素都加上了c
b[x1][y1] += c;
b[x1][y2+1] -= c;
b[x2+1][y1] -= c;
b[x2+1][y2+1] +=c;
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
// 前缀和计算,为构建差分矩阵逆运算
a[i][j] = b[i][j] + a[i-1][j] + a[i][j-1] - a[i-1][j-1];
}
}
for (int i = 1; i <= n; i ++ )
{
for (int j = 1; j <= m; j ++ ) {
cout << a[i][j];
}
cout << endl;
}
return 0;
}
六.双指针算法
1.最长连续不重复子序列
#include <iostream>
using namespace std;
const int N=100010;
int n;
int a[N],cnt[N];// cnt数组记录元素个数
int main(){
cin >> n;
for(int i=0;i<n;i++) {
cin >> a[i];
}
int res=0;// 存储答案
for(int i=0,j=0;i<n;i++){
cnt[a[i]]++;// 对应元素个数加1
while (cnt[a[i]]>1){// 当该元素个数大于1
cnt[a[j]]--;// j指针右移,对应元素个数减1
j++;
}
res = max(res,i-j+1);// 更新最大值
}
cout << res << endl;
return 0;
}
2.数组元素的目标和
O(n+m)
#include <iostream>
using namespace std;
const int N = 100010;
int n,m,x;
int a[N],b[N];
int main(){
cin >> n >> m >> x;
for(int i=0;i<n;i++) cin >> a[i];
for(int i=0;i<m;i++) cin >> b[i];
// 核心在于 j 指针不会回退
for(int i = 0,j = m-1;i < n;i++){
while (a[i] + b[j] > x) j--;
if(a[i] + b[j] == x) {
cout << i << ' ' << j << endl;
break;
}
}
return 0;
}
3.判断子序列
#include <iostream>
using namespace std;
const int N = 100010;
int n,m;
int a[N],b[N];
int main(){
cin >> n >> m;
for(int i=0;i<n;i++) cin >> a[i];
for(int i=0;i<m;i++) cin >> b[i];
int i = 0,j = 0;
// i 指针只在匹配成功下右移
// j 指针不断右移
while (i < n && j < m){
if(a[i] == b[j]) i++;
j++;
}
// i = n 则全部匹配
if(i == n) cout << "Yes" << endl;
else cout << "No" << endl;
return 0;
}
七.位运算
1.二进制中1的个数
#include <iostream>
using namespace std;
int lowbit(int x){
//-x = ~x + 1 (取反加1)
return x & -x;
}
int main(){
int n;
cin >> n;
while (n--){
int x;
cin >> x;
int res=0;// 记录x中1的个数
while(x){
x -= lowbit(x);// 每次消去最后一个1
res++;// 1的个数加1
}
cout << res << ' ';
}
return 0;
}
八.离散化
1.区间和
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef pair<int,int> PII;
const int N=300010;
int n,m;
int a[N];//存储坐标插入的值
int s[N];//存储数组a的前缀和
vector<int> alls;//用来保存真实的下标和想象的下标的映射关系
vector<PII> add,query;//存储插入和询问操作的数据
//二分查找x离散化后的结果
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;
}
//因为是求前缀和,故下标从1开始方便,不用额外的再处理边界
return r+1;
}
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);
}
//alls数组去重
sort(alls.begin(),alls.end());
alls.erase(unique(alls.begin(),alls.end()),alls.end());
//执行前n次插入操作
//for(auto a:b)中b为一个容器,效果是利用a遍历并获得b容器中的每一个值,
//但是a无法影响到b容器中的元素。
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;
}
九.区间合并
1.基本操作
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
typedef pair<int,int> PII;
int n;
vector<PII> intervals,result;// 记录输入区间,合并后区间
bool cmp(PII a,PII b){
return a.first < b.first;
}
int main(){
cin >> n;
for(int i=0;i<n;i++){
int l,r;
cin >> l >> r;
intervals.push_back({l,r});
}
// 以区间左端点为基础进行从小到大排序
sort(intervals.begin(),intervals.end(),cmp);
result.push_back(intervals[0]);//加入第一个区间
for(int i=1;i<intervals.size();i++){
// 这个区间与第一个区间有交集,右端点取max
if(intervals[i].first <= result.back().second){
result.back().second = max(intervals[i].second, result.back().second);
} else{
// 无交集,则加入结果
result.push_back(intervals[i]);
}
}
cout << result.size() << endl;
return 0;
}
(二)第二章-数据结构
一.单链表
1.基本操作
#include <bits/stdc++.h>
using namespace std;
const int N=100010;
int e[N],ne[N],idx,head;
//初始化
void init(){
head=-1;
idx=0;
}
//头插法
void head_insert(int x){
e[idx]=x;
ne[idx]=head;
head=idx++;
}
//在第k个元素后插入x
void insert(int k,int x){
e[idx]=x;
ne[idx]=ne[k];
ne[k]=idx++;
}
//删除第k个元素后的一个数
void remove(int k){
ne[k]=ne[ne[k]];
}
//遍历
for(int i=head;i!=-1;i=ne[i]){
printf("%d ",e[i]);
}
二.双链表
1.基本操作
#include <bits/stdc++.h>
using namespace std;
const int N=100010;
//r[0]为第一个元素,l[1]为最后一个元素
int e[N],l[N],r[N],idx;
void init(){
r[0]=1;// 0是左端点,1是右端点
l[1]=0;
idx=2;
}
// 在节点k的右边插入一个数x
void insert(int k,int x){
e[idx]=x;
r[idx]=r[k];//先从idx连接
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];
}
三.栈
1.基本操作
#include <bits/stdc++.h>
using namespace std;
const int N=100010;
int stk[N],tt;//tt初始为0,stack[0]空着来判空
//入栈
void push(int x){
stk[++tt]=x;
}
//出栈
void pop(){
tt--;
}
//判空
bool empty(){
if(tt==0) return true;
else return false;
}
//栈顶
int query(){
return stk[tt];
}
2.单调栈
#include <bits/stdc++.h>
using namespace std;
const int N=100010;
int n,x;
int stk[N],tt;
//单调栈:一个数的答案只有可能是之前做过答案的数
int main(){
cin >> n;
for(int i=0;i<n;i++){
cin >> x;
while(tt && stk[tt]>=x) tt--;//大的数出栈
if(tt) printf("%d ",stk[tt]);//找到了
else printf("-1 ");
stk[++tt]=x;//入栈
}
return 0;
}
3.表达式求值
#include <iostream>
#include <algorithm>
#include <cstring>
#include <stack>
#include <unordered_map>
using namespace std;
stack<int> num;
stack<char> op;
// 求值函数,使用末尾的运算符操作末尾的两个数
void eval(){
// 先取 b,后取 a
auto b = num.top();num.pop();
auto a = num.top();num.pop();
auto c = op.top();op.pop();
int x;
if(c == '+') x = a + b;
else if(c == '-') x = a - b;
else if(c == '*') x = a * b;
else x = a / b;
num.push(x);
}
int main(){
// 加减法优先级为 1,乘除法优先级为 2
unordered_map<char,int> pr{{'+',1},{'-',1},{'*',2},{'/',2}};
string str;
cin >> str;
for(int i=0;i<str.size();i++){
auto c = str[i];
// 提取数字
if(isdigit(c)){
int x = 0,j = i;
while (j < str.size() && isdigit(str[j])){
x = x * 10 + str[j++] -'0';
}
i = j - 1;
num.push(x);
}
// 左括号直接读入
else if(c == '(') op.push(c);
// 右括号则一直计算,直到遇到左括号并弹出
else if(c == ')'){
while (op.top() != '(') eval();
op.pop();
}
// 一般操作符
else{
// 如果栈顶运算符优先级较高,先操作栈顶元素再入栈
while (op.size() && pr[op.top()] >= pr[c]) eval();
// 如果栈顶运算符优先级较低,直接入栈
op.push(c);
}
}
// 把没有操作完的运算符从右往左操作一遍
while (op.size()) eval();
cout << num.top() << endl;
return 0;
}
四.队列
1.基本操作
#include <bits/stdc++.h>
using namespace std;
const int N=100010;
int q[N],hh,tt=-1;//tt在右为队尾,hh在左为队头
//出队
void push(int x){
q[++tt]=x;
}
//出队
void pop(){
hh++;
}
//判空
bool empty(){
if(tt>=hh) return true;
else return false;
}
//队头元素
int query(){
return q[hh];
}
2.单调队列(滑动窗口)
#include <iostream>
using namespace std;
const int N=1000010;
int n,k;
int a[N],q[N];//队列q存储的是下标
int hh=0,tt=-1;
int main(){
scanf("%d%d",&n,&k);
for(int i=0;i<n;i++) cin >> a[i];
//先找每个窗口的最小值
for(int i=0;i<n;i++){
//如果当前队头在数组的下标小于当前窗口的最小下标则出队
//i是窗口最右边端点,k是窗口长度
if(hh <= tt && q[hh] < i-k+1) hh++;
//新加入的元素更小则舍弃
while (hh <= tt && a[i] <= a[q[tt]]) tt--;
//存入下标
q[++tt] = i;
//窗口里满k个元素才开始输出
if(i >= k-1) printf("%d ",a[q[hh]]);
}
puts("");
//更新队头队尾
hh=0,tt=-1;
//再找每个窗口的最大值
for(int i=0;i<n;i++){
if(hh <= tt && q[hh] < i-k+1) hh++;
//新加入的元素更大则舍弃
while (hh <= tt && a[i] >= a[q[tt]]) tt--;
q[++tt] = i;
if(i >= k-1) printf("%d ",a[q[hh]]);
}
puts("");
return 0;
}
五.KMP
1.基本操作
#include <bits/stdc++.h>
using namespace std;
const int N=100010,M=1000010;
int n,m;
char p[N],s[M];//p为模式串,s为原串
int ne[N];//next数组,全局变量默认初始值为0
int main(){
cin >> n >> p + 1 >> m >> s + 1;//这里的加1是为了从下标为1开始存储
//求next数组(专门针对模式串,仅在p[]中比较,i<=n,ij均遍历模式串)
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;//最长相同前后缀长度
}
//kmp匹配过过程(i<=m,遍历主串)
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){
printf("%d ",i-n);//下标从1开始,不需要+1
j=ne[j];//继续找下一个
}
}
return 0;
}
六.并查集
1.合并集合
#include <bits/stdc++.h>
using namespace std;
const int N=100010;
int n,m;
int p[N];// 记录父节点
//返回x的祖宗结点,并进行路径压缩
int find(int x){
if(p[x]!=x) p[x] = find(p[x]);//找到祖宗结点
else return p[x];
}
int main(){
cin >> n >> m;
for(int i=1;i<=n;i++) p[i] = i;//初始化每个结点祖宗为自己
while(m--){
string op;
cin >> op;
int a,b;
if(op=="M"){
cin >> a >>b;
p[find(a)]=find(b);//a的祖宗结点接到b的祖宗结点下
} else if(op=="Q"){
cin >> a >>b;
if(find(a)==find(b)) printf("Yes\n");
else printf("No\n");
}
}
return 0;
}
2.联通块中点的数量
#include <bits/stdc++.h>
using namespace std;
const int N=100010;
int n,m;
int p[N],cnt[N];//cnt是每个联通子集里元素的个数
//返回x的祖宗结点,并进行路径压缩
int find(int x){
if(p[x]!=x) find(p[x]);//找到祖宗结点
else return p[x];
}
int main(){
cin >> n >> m;
int a,b;
string op;
for(int i=1;i<=n;i++){
p[i]=i;
cnt[i]=1;//个数初始化为1
}
while (m--){
cin >> op;
if(op=="Q1"){
cin >> a >> b;
if(find(a)==find(b)) {
printf("Yes\n");
} else{
printf("No\n");
}
} else if(op=="Q2"){
cin >> a;
printf("%d\n",cnt[find(a)]);
} else if(op=="C"){//在a,b间连接一条边
cin >> a >> b;
if(find(a)!=find(b)){
//这两步不能调换,不然find(a)会改变
cnt[find(b)]+=cnt[find(b)];
p[find(a)]=find(b);
}
}
}
return 0;
}
3.食物链
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 50010;
int n,m;// n 个动物,k 句话
int p[N];// 指向父节点
int d[N];// 到父节点的距离,路径压缩后都变成指向根节点的距离
// 返回 x 的祖宗结点
int find(int x){
if(p[x] != x){
int t = find(p[x]);// 暂存根节点
d[x] += d[p[x]];// 更新距离
p[x] = t;
}
return p[x];
}
int main(){
cin >> n >> m;
// 初始化每个结点祖宗为自己
for(int i=1;i<=n;i++) p[i] = i;
int res = 0;// 假话个数
while (m--){
int t,x,y;
cin >> t >> x >> y;
// 假话条件 2
if(x > n || y > n) res ++;
else{
int px = find(x),py = find(y);// 各自的根节点
// x,y 为同类
if(t == 1){
// 若 x 与 y 在同一个集合中
if(px == py){
if((d[x] - d[y]) % 3) res ++;
} else {
p[px] = py;// x 所在集合合并到 y 所在集合
// (d[x]+ ? -d[y]) % 3==0
d[px] = d[y] - d[x];
}
} else{// x 吃 y
if(px == py){
if((d[x] - d[y] - 1) % 3) res ++;
} else{
p[px] = py;
// (d[x]+ ? -d[y]-1) % 3==0
d[px] = d[y] + 1 -d[x];
}
}
}
}
cout << res << endl;
return 0;
}
七.堆
1.堆排序
#include <bits/stdc++.h>
using namespace std;
const int N=100010;
int n,m;
int h[N],cnt;//cnt是堆里的元素个数
void down(int u){
int t=u;//存储最小值
if(2*u<=cnt && h[2*u]<h[t]) t=u*2;
if(2*u+1<=cnt && h[2*u+1]<h[t]) t=2*u+1;
if(u!=t){//存在比h[u]小的子节点
swap(h[u],h[t]);
down(t);
}
}
void up(int u){
//比父节点小
while(u/2 && h[u/2] > h[u]){
swap(h[u],h[u/2]);
u /= 2;
}
}
//建立小根堆
int main(){
cin >> n >> m;
for(int i=1;i<=n;i++){
cin >>h[i];//从下标1开始存
}
cnt=n;
//从n/2的位置开始down,是因为h[n/2]是最后一个有子结点的结点,之后的所有点都没有子节点
for(int i=n/2;i;i++){
down(i);
}
while(m--){
printf("%d ",h[1]);
//删除最小数
h[1]=h[cnt];
down(1);
cnt--;
}
return 0;
}
2.模拟堆
#include <iostream>
#include <cstring>
using namespace std;
const int N=100010;
int n,m;//m:第几个插入的数
int h[N],cnt;
//ph:第k个插入点对应下标
//hp:该点是第几个插入点
int ph[N],hp[N];
//交换操作还需要交换两个指针
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(2*u<=cnt && h[2*u]<h[t]) t=2*u;
if(2*u+1<=cnt && h[2*u+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/2] > h[u]){
heap_swap(u,u/2);//交换下标
u /= 2;
}
}
int main(){
scanf("%d",&n);
while (n--){
char op[10];
int k,x;
scanf("%s",op);
if(!strcmp(op,"I")){//插入
scanf("%d",&x);
cnt++;//堆元素个数加1
m++;//第m个插入点
ph[m]=cnt,hp[cnt]=m;
h[cnt]=x;
up(cnt);//将该点up上去
} else if(!strcmp(op,"PM")){//输出最小值
printf("%d",h[1]);
} else if(!strcmp(op,"DM")){//删除最小值
heap_swap(1,cnt);
cnt--;
down(1);
} else if(!strcmp(op,"D")){//删除第k个插入点
scanf("%d",&k);
k = ph[k];//找到该点在堆中对应下标
heap_swap(k,cnt);
cnt--;
down(k),up(k);
} else if(!strcmp(op,"C")){
scanf("%d%d",&k,&x);
k = ph[k];
h[k] = x;
down(k),up(k);
}
}
return 0;
}
八.Trie
1.Trie字符串统计
#include <iostream>
using namespace std;
const int N=100010;
int son[N][26];
int cnt[N];// 记录以该点为字符串结尾的数量
int 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 没有子节点 u 则创建
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];
}
return cnt[p];// 返回字符串出现的次数
}
int main(){
int n;
scanf("%d",&n);
while (n--){
char op[2];
scanf("%s%s",&op,&str);
if(op[0]=='I') Insert(str);
else printf("%d\n", Query(str));
}
return 0;
}
2.最大异或对
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010,M = 31 * N;
int n;
int son[M][2];
int idx;
int a[N];
void insert(int x){
int p = 0;// 根节点
// 从最高位开始存储
for(int i=30;i>=0;i--){
int u = x >> i & 1;// 取出二进制数 x 的第 i 位
if(!son[p][u]) son[p][u] = ++idx;// 没有则创建
p = son[p][u];// 转到该子节点
}
}
int query(int x){
int p = 0,res = 0;// res 记录该叶节点是什么
for(int i=30;i>=0;i--){
int u = x >> i & 1;
// 若 u 的反方向的 trie 树枝干被创建则向那个方向走
// 若没有被创建,则先将就一下走 u 的方向
if(son[p][!u]){
p = son[p][!u];
res = res * 2 + !u;
}
else{
p = son[p][u];
res = res * 2 + u;
}
}
return res;
}
int main(){
cin >> n;
for(int i=0;i<n;i++) cin >> a[i];
int res = 0;
for(int i=0;i<n;i++){
// 先插入后运算是为了避免边界问题,对于第一个整数整个树为空
// 若先将自己插入进去,则与自己的异或结果始终为0
insert(a[i]);
// t 为 a[0]至a[i-1] 中与a[i]异或值最大的那个整数
// 即够成局部最大异或对
int t = query(a[i]);
// 看是否是所有整数中的最大异或对
res = max(res,a[i] ^ t);
}
cout << res << endl;
return 0;
}
九.哈希表
1.模拟散列表
1.拉链法
#include <iostream>
#include <cstring>
using namespace std;
const int N=100003;// 取大于1e5的第一个质数,取质数冲突的概率最小
int n,x;
int h[N];
int e[N],ne[N],idx;
// 拉链法,其实就是图论中的领接表存储
void Insert(int x){
int k = (x % N + N) % 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(){
scanf("%d",&n);
memset(h,-1,sizeof h);
while (n--){
char op[2];
scanf("%s%d",&op,&x);
if(op[0]=='I'){
Insert(x);
} else if(op[0]=='Q'){
if(find(x)) puts("Yes");
else puts("No");
}
}
return 0;
}
2.开放寻址法
#include <iostream>
#include <cstring>
using namespace std;
const int N=200003,null=0x3f3f3f3f;
int n,x;
int h[N];
// 开放寻址法,返回x所在位置
int find(int x){
int k = (x % N + N) % N;// 得到哈希值
// 该位置上有值并且不等于x则循环
while (h[k]!=null && h[k]!=x){
k++;
if(k==N) k=0;// 查找找到最右端后,换到最左端查找
}
// 若x在表中,则返回该位置
// 若x不在表中,返回插入位置
return k;
}
int main(){
scanf("%d",&n);
// 设定一个不包含的值代表该格子为空
memset(h,0x3f,sizeof h);
while (n--){
char op[2];
scanf("%s%d",&op,&x);
int k= find(x);// 找到对应位置
if(op[0]=='I') h[k]=x;// 插入x值
else if(op[0]=='Q'){
if(h[k] != null) puts("Yes");
else puts("No");
}
}
return 0;
}
2.字符串哈希
#include <iostream>
using namespace std;
typedef unsigned long long ULL;
// P = 131 或 13331 Q=2^64,在99%的情况下不会出现冲突
const int N=100010,P=131;
int n,m;
char str[N];
ULL h[N];// h[i]前i个字符的hash值
ULL p[N];// p[i]存放各个位数的相应权值P^i
ULL get(int l,int r){
// 这步将h[l-1]左移
// 使得h[l-1]的高位与h[r]相对齐再计算
return h[r] - h[l-1] * p[r-l+1];
}
int main(){
scanf("%d%d",&n,&m);
p[0]=1;// P^0 = 1
for(int i=1;i<=n;i++){
p[i] = p[i-1] * P;// 该位置上所乘的系数P^i
h[i] = h[i-1] * P + str[i];// 前缀和公式
}
while (m--){
int l1,r1,l2,r2;
scanf("%d%d%d%d",&l1,&r1,&l2,&r2);
// 哈希值相等则对应字符串相同
if(get(l1,r1) == get(l2,r2)) puts("Yes");
else puts("No");
}
return 0;
}
(三)第三章-图论
一.树和图的深度优先遍历
1.dfs基本操作
#include <bits/stdc++.h>
using namespace std;
const int N=100010;
//领接表法存储图,每个结点后面都是一串链表
int n,m;
int h[N];//h[N]存储每个点作为头结点
int e[2*N],ne[2*N],idx;//无向图,N个节点,最多2*N条边
bool st[N];//记录结点是否已经走过
//将b插入a结点后面,代表a,b间有边
void add(int a,int b){
e[idx]=b;
ne[idx]=h[a];
h[a]=idx++;
}
//深度优先遍历
void dfs(int u){
st[u]=true;//已经遍历这个点
for(int i=h[u];i!=-1;i=ne[i]){
int j=e[i];
if(!st[j]) dfs(j);//遍历所有未被遍历的连接点
}
}
int main(){
memset(h,-1,sizeof h);//所有头结点初始化为-1
dfs(1);
return 0;
}
2.树的重心
# include <iostream>
# include <algorithm>
# include <cstring>
using namespace std;
const int N=100010;
int n;
int h[N];
int e[2*N],ne[2*N],idx;
bool st[N];
int ans = N;//存储答案
//返回以u为根的子树中结点的数量
void add(int a,int b){
e[idx]=b;
ne[idx]=h[a];
h[a]=idx++;
}
//求树的重心,删除重心的剩余连通块的最大值最小
//并且输出该最大值
int dfs(int u){
st[u]= true;
//res是表示将u点去除后,剩下的子树中数量的最大值
//sum表示以u为根的子树的点的多少,初值为1,因为已经有了u这个点
int sum=1,res=0;
for(int i=h[u];i!=-1;i=ne[i]){
int j=e[i];
if(!st[j]){
int s=dfs(j);//s为u点下面以j为结点的连通块的点数
res = max(res,s);//这是在求u点之下连通块的点数,来找一个最大值
sum += s;//累加u结点下每个子树和
}
}
res = max(res,n-sum);//与u点上面连通块的点数,取个最大值
ans = min(ans,res);//这是求答案,根据题目,求删掉各个点后,最小的最大连通块点数
return sum;
}
int main(){
int a,b;
memset(h,-1,sizeof h);
cin >> n;
for(int i=0;i<n;i++){
cin >> a >> b;
add(a,b),add(b,a);
}
dfs(1);
printf("%d", ans);
return 0;
}
二.树和图的广度优先遍历
1.图中点的层次
#include <bits/stdc++.h>
using namespace std;
const int N=100010;
int n,m;
int h[N],e[N],ne[N],idx;//本题是单向图,不需要开2*N
int d[N],q[N];//d[N]是到各点的最短距离,q[N]是辅助队列
void add(int a,int b){
e[idx]=b;
ne[idx]=h[a];
h[a]=idx++;
}
int bfs(){
int hh=0,tt=0;//初始化队列
q[0]=1;//插入辅助队列第一个点
memset(d,-1,sizeof d);//初始化距离
d[1]=0;//第一个点到自己距离为0
while(hh<=tt){//队列非空
int t=q[hh++];//出队
for(int i=h[t];i!=-1;i=ne[i]){
int j=e[i];
if(d[j]==-1){
d[j]=d[t]+1;//路径长度加1
q[++tt]=j;//没有遍历过的点入队
}
}
}
return d[n];
}
int main(){
cin >> n >> m;
int a,b;
memset(h,-1,sizeof h);//初始化头结点为-1
for(int i=0;i<m;i++){
cin >> a >> b;
add(a,b);
}
printf("%d",bfs());
return 0;
}
三.DFS
1.排列数字
#include <bits/stdc++.h>
using namespace std;
const int N=100010;
int n;
int path[N];//保存序列
bool st[N];//记录是否经过
void dfs(int u){
if(u > n){//已经经过n个元素,全部输出
for(int i=1;i<=n;i++){
printf("%d ",path[i]);
}
puts("");//换行
}
for(int i=1;i<=n;i++){//可以选择的数字为1-n
if(!st[i]){//如果数字i没有被用过
path[u]=i;//记录该点
st[i]= true;
dfs(u+1);//遍历下一层
path[u]=0;//回溯还原,找其他可能结果
st[i]=false;
}
}
}
int main(){
cin >> n;
dfs(1);
return 0;
}
2.n皇后问题
解法一
#include <bits/stdc++.h>
using namespace std;
const int N = 20;//由于要存对角线,所以存储体积翻倍
int n;
char g[N][N];//记录棋盘上每个点的值
bool col[N],dg[N],udg[N];//col列,dg对角线,udg反对角线
//这里一行一行遍历,遍历完n行截止
void dfs(int x){
if(x==n){//已经遍历了n行
for(int i=0;i<n;i++) puts(g[i]);//这里输出的是一行
puts("");//换行并返回
}
for(int y=0;y<n;y++){
//该点的列,对角线,反对角线都没有元素
if(!col[y] && !dg[x+y] && !udg[y-x+n]){
g[x][y]='Q';//赋值
col[y]=dg[x+y]=udg[y-x+n]= true;//注释这些点已经使用过了
dfs(x+1);//遍历下一行
g[x][y]='.';//回溯复原
col[y]=dg[x+y]=udg[y-x+n]= false;
}
}
}
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 row[N],col[N],dg[N],udg[N];
//一个一个点去遍历,s是已经安排好的皇后个数
void dfs(int x,int y,int s){
if(y==n) x++,y=0;//遍历每一行出界,要修改x,y值
if(x==n){//已经遍历了n行
if(s==n){//安排好了n个皇后
for(int i=0;i<n;i++) puts(g[i]);
puts("");
}
return;
}
//不放皇后
dfs(x,y+1,s);
//放皇后
if(!row[x] && !col[y] && !dg[x+y] && !udg[y-x+n]){
g[x][y]='Q';
row[x]=col[y]=dg[x+y]=udg[y-x+n]= true;
dfs(x,y+1,s+1);//由于是一个一个遍历,所以继续往后找
g[x][y]='.';//回溯复原
row[x]=col[y]=dg[x+y]=udg[y-x+n]= 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;
}
四.BFS
1.走迷宫
#include <bits/stdc++.h>
using namespace std;
typedef pair<int,int> PII;//定义每个点坐标形式
const int N=110;
int n,m;
int g[N][N];//记录图
int d[N][N];//记录每个点到原点距离
PII q[N*N];//辅助队列,记录经过的点
PII Prev[N][N];//记录该点的前一个点,便于输出路径
PII stk[N*N];
int bfs(){
int hh=0,tt=0;
q[0]={0,0};
memset(d,-1,sizeof d);//初始化距离-1
d[0][0]=0;
//x,y方向拓展向量,搜素上下左右
int dx[4]={-1,0,1,0},dy[4]={0,1,0,-1};
while(hh<=tt){
auto t=q[hh++];//出队,auto自动判断格式
//遍历四个方向
for(int i=0;i<4;i++){
int x=t.first+dx[i],y=t.second+dy[i];
//在范围内,g[][]==0可以到达,d[][]==-1表示第一次经过,为最短距离
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;//距离加1
Prev[x][y]=t;//记录前一个点
q[++tt]={x,y};
}
}
}
//这里用栈存了倒过来经过的元素,可以正过来输出路径
int x=n-1,y=m-1,dd=0;
while(x || y){//x,y不同时为0,就没到起点
stk[++dd]={x,y};
auto t=Prev[x][y];
x=t.first,y=t.second;//继续往前输出路径
}
printf("(%d,%d)\n",0,0);//起始元素
while(dd!=0){
printf("(%d,%d)\n",stk[dd].first,stk[dd].second);
dd--;
}
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++){
scanf("%d",&g[i][j]);
}
}
printf("%d",bfs());
return 0;
}
2.八数码
#include <iostream>
#include <algorithm>
#include <unordered_map>
#include <queue>
using namespace std;
queue<string> q;// 定义队列
unordered_map<string,int> d;// 通过哈希表来让字符串变化时和移动的距离数值关联,d[string] = int
int dx[4]={-1,0,1,0},dy[4]={0,1,0,-1};// 移动方向
int bfs(string start){
string end = "12345678x";// 设定字符串终止状态
q.push(start);// 字符串入队
d[start] = 0;// 初始状态移动步数为 0
while (q.size()){
// 取出队头字符串
auto t = q.front();
q.pop();
// 如果当前字符串等于终止状态,则返回该字符串移动的次数
int distance = d[t];
if(t == end) return distance;
// k 表示 'x' 字符在字符串当前的下标
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){
// 将字符串中的搜索位置与字符'x'交换
swap(t[k],t[a * 3 + b]);
// 如果当前状态是第一次遍历,记录距离,入队
if(!d.count(t)){
d[t] = distance + 1;
q.push(t);
}
// 恢复原状态,返回位置判断其他方向
swap(t[k],t[a * 3 + b]);
}
}
}
return -1;
}
int main(){
string start;
for(int i=0;i<9;i++){
char c;
cin >> c;
start += c;// 逐个输入字符串
}
cout << bfs(start) << endl;
return 0;
}
五.拓扑排序
1.拓扑序列
#include <bits/stdc++.h>
using namespace std;
const int N=100010;
int n,m;
int h[N];//每个点作头结点
int e[N],ne[N],idx;//领接表法,模拟链表
int q[N],d[N];//q[N]模拟队列,d[N]记录每个点的入度
void add(int a,int b){
e[idx]=b;
ne[idx]=h[a];
h[a]=idx++;
}
//拓扑排序
bool top_sort(){
int hh=0,tt=-1;
for(int i=1;i<=n;i++){
if(d[i]==0){
q[++tt]=i;//入度为0的点i入队
}
}
while(hh<=tt){
int t=q[hh++];//队头出队
for(int i=h[t];i!=-1;i=ne[i]){
int j=e[i];//这里存点的是e[i],而非i
d[j]--;//出队了j的前一个结点,入度减1
if(d[j]==0){
q[++tt]=j;//若此时入度为0,则入队
}
}
}
return tt==n-1;//若n个元素全部已经入队,输出
}
int main(){
cin >> n >> m;
memset(h,-1,sizeof h);
int x,y;
while(m--){
cin >> x >> y;
add(x,y);
d[y]++;//增加了一条由x指向y的边,y入度加1
}
if(top_sort()){//存在拓扑排序
for(int i=0;i<n;i++){
printf("%d ",q[i]);//队列里即为拓扑排序
}
} else{
printf("-1\n");
}
return 0;
}
六.Dijkstra
0.最短路径问题方法
1. 基本操作
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N=510;
int n,m;
int g[N][N];//稠密图用领接矩阵存储
int dist[N];//记录每个点到起点距离
bool st[N];//是否确定最短路
int dijkstra(){
memset(dist,0x3f,sizeof dist);//初始值为无穷大
dist[1]=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;//找到未确定最短路中的最小值
}
}
st[t]= true;//确定最短路
for(int j=1;j<=n;j++){
dist[j] = min(dist[j],dist[t]+g[t][j]);
}
}
// 0x3f 0x3f3f3f3f 的区别?
// memset按字节赋值,所以memset 0x3f 就等价与赋值为0x3f3f3f3f
if(dist[n]==0x3f3f3f3f){
return -1;//不连通
} else{
return dist[n];
}
}
int main(){
scanf("%d%d",&n,&m);
memset(g,0x3f,sizeof g);
while(m--){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
g[a][b]=min(g[a][b],c);//取该边权重与最短路较小值
}
int t = dijkstra();
printf("%d\n",t);
return 0;
}
2.堆优化版
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
typedef pair<int,int> PII;
const int N=150010;
int n,m;
int h[N],w[N],e[N],ne[N],idx;//w存储权重
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 dijkstra(){
memset(dist,0x3f,sizeof dist);
dist[1]=0;
priority_queue<PII,vector<PII>,greater<PII>> heap;// 建立小根堆
heap.push({0,1});//插入距离和节点编号
while(heap.size()){//堆不为空
auto t=heap.top();//取距离原点最近的点
heap.pop();
int ver=t.second,distance=t.first;//ver:节点编号,distance:源点距离ver 的距离
if(st[ver]) continue;//已经处理过了
st[ver]= true;
for(int i=h[ver];i!=-1;i=ne[i]){//更新ver所指向的节点距离
int j=e[i];
if(dist[j] > distance + w[i]){
dist[j] = distance + 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);
int a,b,c;
while(m--){
cin >> a >> b >> c;
add(a,b,c);
}
int t=dijkstra();
printf("%d\n",t);
return 0;
}
七.Bellman-ford
1.基本操作
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N=510,M=10010;//N为点,M为边
int n,m,k;
int dist[N];//每个点到原点距离
int backup[N];//备份dist数组
struct Edge{//结构体存储所有边,便于遍历
int a,b,w;
}edges[M];
int bellman_ford(){
memset(dist,0x3f,sizeof dist);
dist[1]=0;
for(int i=0;i<k;i++){
//将backup备份dist上一次迭代的结果,防止发生串联
memcpy(backup,dist,sizeof dist);
for(int j=0;j<m;j++){//遍历每一条边
int a=edges[j].a,b=edges[j].b,w=edges[j].w;
dist[b] = min(dist[b],backup[a]+w);//关键比较
}
}
if(dist[n] > 0x3f3f3f3f / 2) puts("impossible");
printf("%d\n",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};
}
bellman_ford();
return 0;
}
八.Spfa
1.基本操作
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
const int N=150010;
int n,m;
int h[N],w[N],e[N],ne[N],idx;
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();
// 从队列中取出来之后该节点被标记为false,
// 代表之后该节点如果发生更新可再次入队
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) puts("impossible");
else printf("%d\n",dist[n]);
}
int main(){
cin >> n >> m;
memset(h,-1,sizeof h);
int a,b,c;
while(m--){
cin >> a >> b >> c;
add(a,b,c);
}
spfa();
return 0;
}
2.判断负环
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
const int N=150010;
int n,m;
int h[N],w[N],e[N],ne[N],idx;
int dist[N],cnt[N];//cnt存储到该点经过的边数
bool st[N];//记录这个点是否在队列中
void add(int a,int b,int c){
e[idx]=b;
w[idx]=c;//权重赋值
ne[idx]=h[a];
h[a]=idx++;
}
bool spfa(){
//为什么dist数组不用初始化呢
//因为有负环的存在所以最终某些点的dist是数组的状态肯定是无穷小的,
//而无论dist的初始值为多少肯定都会往无穷小的方向区无限次的更新
queue<int> q;
for(int i=1;i<=n;i++){//所有点入队
st[i]= true;
q.push(i);
}
while(q.size()){
int t=q.front();
q.pop();
// 从队列中取出来之后该节点被标记为false,
// 代表之后该节点如果发生更新可再次入队
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;//该点路径长度为前一个距离加一
//路径长度大于等于n,则一定经过n+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);
int a,b,c;
while(m--){
cin >> a >> b >> c;
add(a,b,c);
}
bool ret=spfa();
if(ret) puts("Yes");
else puts("No");
return 0;
}
九.Floyd
1.基本操作
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N=210,INF=1e9;
int n,m,Q;//Q为查询次数
int d[N][N];//存储图
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 >> Q;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(i==j) d[i][j]=0;//自环距离为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(Q--){
int a,b;
cin >> a >> b;
//由于有负权边存在所以大于INF/2即可
if(d[a][b] > INF/2) puts("impossible");
else printf("%d\n",d[a][b]);
}
return 0;
}
2.最短路总结
十.Prim
1.基本操作
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
const int N=510,INF=0x3f3f3f3f;
int n,m;
int g[N][N];
int dist[N];//存储各个节点到生成树的距离
bool st[N];//节点是否被加入到生成树中
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];//更新长度
st[t]= true;
for(int j=1;j<=n;j++){//更新生成树外的点到生成树的距离
dist[j]=min(dist[j],g[t][j]);
}
}
return res;
}
int main(){
cin >> n >> m;
memset(g,0x3f,sizeof g);
while(m--){
int u,v,w;
cin >> u >> v >> w;
g[u][v]=g[v][u]=min(g[u][v],w);//无向图
}
int t = prim();
if(t == INF) puts("impossible");
else printf("%d\n",t);
return 0;
}
十一.Kruskal
1.基本操作
#include <iostream>
#include <algorithm>
using namespace std;
const int N=200010;
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(){
// res记录最小生成树的树边权重之和,
// cnt记录的是全部加入到树的集合中边的数量(可能有多个集合)
int res=0,cnt=0;
for(int i=0;i<m;i++){
int a=edges[i].a,b=edges[i].b,w=edges[i].w;
// 如果a和b已经在一个集合当中了,说明这两个点已经被一种方式连接起来了,
// 如果加入a-b这条边,会导致集合中有环的生成
if(find(a)!= find(b)){
p[find(a)]= find(b);//将a,b所在的两个集合连接起来
cnt++;//因为加入的是a-b的这一条边,将a,b所在的两个集合连接之后,全部集合中的边数加1
res+=w;//加入到集合中的边的权重之和
}
}
//树中有n个节点便有n-1条边,如果cnt不等于n-1的话,说明无法生成有n个节点的树
if(cnt==n-1) return res;
else return 0x3f3f3f3f;
}
int main(){
cin >> n >> m;
for(int i=1;i<=n;i++) p[i]=i;//初始化并查集
for(int i=0;i<m;i++){
int a,b,w;
cin >> a >> b >> w;
edges[i]={a,b,w};
}
sort(edges,edges+m);//将边的权重按照大小一一排序
int t=Kruskal();
if(t==0x3f3f3f3f) puts("impossible");
else printf("%d\n",t);
return 0;
}
十二.染色法判断二分图
1.基本操作
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N=100010,M=200010;
int n,m;
int h[N],e[M],ne[M],idx;
int color[N];//保存各个点的颜色,0 未染色,1 是红色,2 是黑色
void add(int a,int b){
e[idx]=b;
ne[idx]=h[a];
h[a]=idx++;
}
//返回是否可以成功将u染色为c
bool dfs(int u,int c){
color[u]=c;//修改当前颜色
//尝试染链接边的颜色
for(int i=h[u];i!=-1;i=ne[i]){
int j=e[i];
//j未染色
if(!color[j]){
if(!dfs(j,3-c)) return false;//染成另一种颜色
}
//j已染色
else if(color[j]==c) return false;//出现奇数环导致j与i同色
}
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]){//若i未染色
if(!dfs(i,1)){//从i开始深度优先遍历图,逐个染色
flag= false;//出现矛盾,染色失败
break;
}
}
}
if(flag) puts("Yes");
else puts("No");
return 0;
}
十三.匈牙利算法
1.基本操作
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N=510,M=100010;
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){
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++;//匹配成功
}
printf("%d\n",res);
return 0;
}
(四)第四章-数学
一.质数
1.试除法
#include <iostream>
using namespace std;
// 试除法判断质数
bool is_prime(int n){
if(n == 1) return false;// 1不是质数
// 一直到除到 n/i 为止(相当于是到根号n,不推荐用sqrt函数)
for(int i=2;i < n/i;i++){
if(n % i==0) return false;
}
return true;
}
int main(){
int n,x;
scanf("%d",&n);
while (n--){
scanf("%d",&x);
if(is_prime(x)) puts("Yes");
else puts("No");
}
return 0;
}
2.分解质因数
#include <iostream>
using namespace std;
void divide(int n){
for(int i=2;i <= n/i;i++){
// n不包含任何从2到i-1之间的质因子(已经被除干净了)
// 所以i也不包含何从2到i-1之间的质因子,保证了i是质数
if(n % i == 0){// i为底数
int s=0;//s 为指数
while (n % i == 0){
n /= i;
s++;
}
printf("%d %d",i,s);
}
}
// 最多只有一个大于根下n的质因子(两个相乘就大于n了)
if(n > 1) printf("%d %d\n",n,1);
puts("");
}
int main(){
int n,x;
scanf("%d",&n);
while (n--){
scanf("%d",&x);
divide(x);
}
return 0;
}
3.筛质数
最普通的筛法(用任意数筛合数) O(nlogn)
#include <iostream>
using namespace std;
const int N=1000010;
int primes[N],cnt;// cnt为质数个数
bool st[N];// st[x]存储x是否被筛掉
void get_primes(int n){
for(int i=2;i<=n;i++){
if(!st[i]){
primes[cnt++] = i;// 存储质数
}
// 把i的倍数都筛掉
for(int j=i+i;j<=n;j+=i) st[j]= true;
}
}
int main(){
int n;
scanf("%d",&n);
get_primes(n);
printf("%d", cnt);
return 0;
}
埃氏筛法 (用质数筛合数) O(nloglogn)
void get_primes1(){
for(int i=2;i<=n;i++){
if(!st[i]){
primes[cnt++]=i;
// 用质数就把所有的合数都筛掉
// 合数要筛的数必定已经被它的因数筛过了
for(int j=i;j<=n;j+=i) st[j]=true;
}
}
}
线性筛法(用最小质因数筛合数) O(n)
// 线性筛法,每个数只会被最小质因子筛“一遍”
void get_primes(int n){
for(int i=2;i<=n;i++){
if(!st[i]) primes[cnt++] = i;
// 从小到大取出质数,取到sqrt[n]为止
for(int j=0;primes[j] <= n/i;j++){
// 筛掉质数与i的乘积
st[primes[j] * i] = true;
// primes[j]一定是i的最小质因子
if(i % primes[j] == 0) break;
}
}
}
二.约数
1.试除法
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
vector<int> get_divisors(int n){
vector<int> res;
// i为约数,n/i也为约数
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 n;
scanf("%d",&n);
while (n--){
int x;
scanf("%d",&x);
auto res= get_divisors(x);// 用auto变量更简洁
for(auto t:res) printf("%d ",t);
printf("\n");
}
return 0;
}
2.约数个数 
#include <iostream>
#include <algorithm>
#include <unordered_map>
using namespace std;
const int mod=1e9+7;
int main(){
int n;
cin >> n;
unordered_map<int,int> h;// 质因子及其指数
while (n--){
int x;
cin >> x;
// 依次求出各质因子对应指数
for(int i=2;i <= x/i;i++){
while (x % i == 0){
// 把所有对应指数累加
h[i]++;
x /= i;
}
}
// x的最大质因子可能大于sqrt(x)
if(x>1) h[x]++;
}
long long res=1;// 防止超int
for(auto i = h.begin();i != h.end();i++){
// 计算公式 res = (x1+1)(x2+1)(x3+1)…(xk+1)
res = res * (i->second + 1) % mod;
}
cout << res << endl;
return 0;
}
3.约数之和
#include <iostream>
#include <algorithm>
#include <unordered_map>
using namespace std;
const int mod=1e9+7;
int main(){
int n;
cin >> n;
unordered_map<int,int> h;// 质因子及其指数
while (n--){
int x;
cin >> x;
// 依次求出各质因子对应指数
for(int i=2;i <= x/i;i++){
while (x % i == 0){
// 把所有对应指数累加
h[i]++;
x /= i;
}
}
// x的最大质因子可能大于sqrt(x)
if(x>1) h[x]++;
}
long long res=1;// 防止超int
for(auto i = h.begin();i != h.end();i++){
// p为底数,a为指数
int p = i->first,a = i->second;
long long t=1;// 计算 p^0+……+p^a
while (a--){
t = (t*p+1) % mod;
}
// 累乘得到结果
res = res * t % mod;
}
cout << res << endl;
return 0;
}
4.最大公约数
#include <iostream>
using namespace std;
// 求两个正整数 a 和 b 的 最大公约数 d
// 则有 gcd(a,b) = gcd(b,a%b)
int gcd(int a,int b){
if(b == 0) return a;
return gcd(b,a % b);
}
int main(){
int n;
cin >> n;
while (n--){
int a,b;
cin >> a >> b;
cout << gcd(a,b) << endl;
}
return 0;
}
三.欧拉函数
1.欧拉函数
#include <iostream>
using namespace std;
int main(){
int n;
cin >> n;
while (n--){
int a;
cin >> a;// 欧拉公式中的系数 N
int res = a;// 记录答案
// 求质因子
for(int i=2;i<=a/i;i++){
if(a % i == 0){// 找到质因子
// (p - 1) / p
// 先除后乘(乘积可能会超出int范围)
res = res / i * (i-1);
// 对 n 进行约分
while (a % i == 0) a /= i;
}
}
// 如果有剩余,则剩余是个质因子
if(a>1) res = res / a * (a-1);
cout << res << endl;
}
return 0;
}
2.筛法求欧拉函数
#include <iostream>
using namespace std;
typedef long long LL;
const int N=1000010;
// primes存放已经找到的质数
// st[]标记某个数是不是质数
// cnt记录已经找到的质数数量
int primes[N],cnt;
int phi[N];// 欧拉函数 Φ(x)
bool st[N];
// 线性筛法改编
LL get_eulers(int n){
phi[1]=1;
for(int i=2;i<=n;i++){
if(!st[i]){
primes[cnt++] = i;
// 1到质数i中与i互质的数个数为i-1
phi[i] = i-1;
}
for(int j=0;primes[j]<=n/i;j++){
st[primes[j] * i] = true;
if(i % primes[j] == 0){
/*
当p[j]是i的一个约数时,i的质因数与p[j]*i的质因数完全相同。
i = (p1^a1)*(p2^a2)*(p3^a3)*...(p[j]^ap[j])...(pk^ak)
i*p[j] = (p1^a1)*(p2^a2)*(p3^a3)*...(p[j]^(ap[j]+1))...(pk^ak)
由欧拉函数的定义可知:
Φ(i) = i * (1-1/p1)*(1-1/p2)*(1-1/p3)*...(1-1/p[j])*...(1-1/pk)
Φ(i*p[j]) = i*p[j]*(1-1/p1)*(1-1/p2)*(1-1/p3)*...(1-1/p[j])*...(1-1/pk)
*/
phi[primes[j] * i] = phi[i] * primes[j];
break;
}
/*
当i%p[j]!=0时,p[j]不是i的约数,i与i*p[j]的质因子相差一个p[j]
i = (p1^a1)*(p2^a2)*(p3^a3)*...(pk^ak)
i*p[j] = (p1^a1)*(p2^a2)*(p3^a3)*...(pk^ak)*(p[j]^ap[j])
所以由欧拉函数的定义可知:
Φ(i) = i *(1-1/p1)*(1-1/p2)*(1-1/p3)*...(1-1/pk)
Φ(i*p[j]) = i*p[j]*(1-1/p1)*(1-1/p2)*(1-1/p3)*...(1-1/pk)(1-1/p[j])
*/
phi[primes[j] * i] = phi[i] * (primes[j] - 1);//p[j]*(p[j]-1)/p[j]*phi[i] p[j]约了
}
}
LL res=0;
for(int i=1;i<=n;i++) res += phi[i];
return res;
}
int main(){
int n;
cin >> n;
cout << get_eulers(n) << endl;
return 0;
}
四.快速幂
1.快速幂
#include <iostream>
using namespace std;
typedef long long LL;
// a^k % p
LL qmi(LL a,int k,int p){
LL res=1;
// 对k进行二进制化,从低位到高位
while (k){
// 如果k的二进制表示的第0位为1,则乘上当前的a
// b&1,若为1,b&1=true,反之b&1=false
if(k & 1) res = res * a % p;
// k右移一位
k = k >> 1;
// 更新a,a依次为a^{2^0},a^{2^1},a^{2^2},....,a^{2^logb}
a = a * a % p;
}
return res;
}
int main(){
int n;
scanf("%d",&n);
while (n--){
int a,k,p;
scanf("%d%d%d",&a,&k,&p);
printf("%lld\n", qmi(a,k,p));
}
return 0;
}
2.快速幂求逆元
#include <iostream>
using namespace std;
typedef long long LL;
// a^k % p
LL qmi(LL a,int k,int p){
LL res=1;
// 对k进行二进制化,从低位到高位
while (k){
// 如果k的二进制表示的第0位为1,则乘上当前的a
// b&1,若为1,b&1=true,反之b&1=false
if(k & 1) res = res * a % p;
// k右移一位
k = k >> 1;
// 更新a,a依次为a^{2^0},a^{2^1},a^{2^2},....,a^{2^logb}
a = a * a % p;
}
return res;
}
int main(){
int n;
scanf("%d",&n);
while (n--){
int a,p;
scanf("%d%d",&a,&p);
int res = qmi(a,p-2,p);
// 若a是p的倍数,则无解
// 因为a,p互质才能由欧拉定理推出一定有逆元
if(a % p) printf("%d\n",res);
else puts("impossible");
}
return 0;
}
五.扩展欧几里得算法
1.扩展欧几里得算法
#include <iostream>
using namespace std;
// a * x + b * y = gcd(a,b)
void exgcd(int a,int b,int &x,int &y){
if(b == 0){
x = 1,y = 0;// 如果b=0,则gcd(a, b) = 1 * a + 0 * b
}else{
exgcd(b,a % b,x,y);
// 修改x,y值
int t = x;
x = y;
y = t - a / b * y;
}
}
int main(){
int n;
scanf("%d",&n);
while (n--){
int a,b,x,y;
scanf("%d%d",&a,&b);
exgcd(a,b,x,y);// x,y分别为a,b的系数
printf("%d %d\n",x,y);
}
return 0;
}
2.线性同余方程
#include <iostream>
using namespace std;
// a * x + b * y = gcd(a,b)
int exgcd(int a,int b,int &x,int &y){
if(b == 0){
x = 1,y = 0;// 如果b=0,则gcd(a, b) = 1 * a + 0 * b
return a;
}
int d = exgcd(b,a % b,x,y);
// 修改x,y值
int t = x;
x = y;
y = t - a / b * y;
return d;
}
int main(){
int n;
scanf("%d",&n);
while (n--){
int a,b,m;
scanf("%d%d%d",&a,&b,&m);
int x,y;
int d = exgcd(a,m,x,y);
if(b % d) puts("impossible");
else printf("%d\n",(long long )x * (b / d) % m);
}
return 0;
}
六.中国剩余定理
1.表达整数的奇怪方式
七.高斯消元
1.线性方程组
#include <iostream>
#include <math.h>
using namespace std;
const int N=110;
const double eps = 1e-6;// 浮点数有误差,小于此数判定为0
int n;
double a[N][N];
int gauss(){
int c,r;// 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;
}
}
// 当前列的最大行是 0,就没有必要再计算下去了
if(fabs(a[t][c]) < eps) continue;
// 将这行换到最上面
for(int i=c;i<=n;i++) swap(a[t][i],a[r][i]);
// 将这行第一个数变为 1,注意倒过来更新
for(int i=n;i>=c;i--) a[r][i] /= a[r][c];
// 将下面所有行第 c 列变为 0
for(int i=r+1;i<n;i++){
// 不是 0 才进行操作,注意每一行依然是倒过来操作
if(fabs(a[i][c]) > eps){
for(int j=n;j>=c;j--){
a[i][j] -= a[r][j] * a[i][c];
}
}
}
// 当前行已确定,搞下一行
r++;
}
// 说明剩下方程的个数是小于 n 的,说明不是唯一解,判断是无解还是无穷多解
// 因为已经是阶梯型,所以 r ~ n-1 的值应该都为 0
if(r < n){
for(int i=r;i<n;i++){
// 如果出现左边不等于0,无解
if(fabs(a[i][n]) > eps){
return 2;// 无解
}
}
// 0=0,有无穷解(说明有的方程是别的方程演化而来)
return 1;// 无穷解
}
// 将其余系数全变为0
// 唯一解 ↓,从下往上回代,得到方程的解
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(){
scanf("%d",&n);
for(int i=0;i<n;i++){
for(int j=0;j<n+1;j++){
scanf("%lf",&a[i][j]);
}
}
// 0 表示唯一解,1 表示无穷解,2 表示无解
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;
}
2.异或线性方程组
八.求组合数
1.方法一
#include <iostream>
using namespace std;
const int N = 110,mod = 1e9+7;
int c[N][N];
void init(){
for(int i=0;i<N;i++){
for(int j=0;j<=i;j++){
// 组合数Cij,j在上为0,则组合数为1
if(j == 0) c[i][j] = 1;
else{
// 递推公式
c[i][j] = (c[i-1][j] + c[i-1][j-1]) % mod;
}
}
}
}
int main(){
init();
int n;
scanf("%d",&n);
while (n--){
int a,b;
scanf("%d%d",&a,&b);
printf("%d\n",c[a][b]);
}
return 0;
}
2.方法二
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 100010,mod = 1e9+7;
int fact[N],infact[N];// 阶乘/阶乘逆元 % mod
// a^k % p 快速幂求逆元
LL qmi(LL a,int k,int p){
LL res=1;
// 对k进行二进制化,从低位到高位
while (k){
// 如果k的二进制表示的第0位为1,则乘上当前的a
// b&1,若为1,b&1=true,反之b&1=false
if(k & 1) res = res * a % p;
// k右移一位
k = k >> 1;
// 更新a,a依次为a^{2^0},a^{2^1},a^{2^2},....,a^{2^logb}
a = a * a % p;
}
return res;
}
int main(){
fact[0] = infact[0] = 1;// 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);
// 组合数公式(计算期间注意模运算防止爆int)
printf("%d\n",(LL)fact[a] * infact[b] % mod * infact[a-b] % mod);
}
return 0;
}
3.方法三
4.方法四(高精度)
#include <iostream>
#include <vector>
using namespace std;
const int N=5010;
int primes[N],cnt;
bool st[N];
int sum[N];// 组合数阶乘里面 x 的个数 sum[x]
// 线性筛质数
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[i * primes[j]] = true;
if(i % primes[j] == 0) break;
}
}
}
// 求 n! 里面包含 p 的个数
int get(int n,int p){
int res=0;
while (n){
res += n/p;// 累加 n/p,n/p^2……的个数
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;// t + A[i] * b = 7218
C.push_back(t%10);// 只取个位 8
t /= 10;// 721 看作进位
}
// 处理最后剩余的t
while(t){
C.push_back(t%10);
t /= 10;
}
// 删除前面的0,也保留了可能为答案的0
while(C.size()>1 && C.back()==0) C.pop_back();
return C;
}
int main(){
int a,b;
cin >> a >>b;
get_primes(a);
for(int i=0;i<cnt;i++){
int p=primes[i];
// 组合数公式求质数 p 的个数
sum[i] = get(a,p) - get(b,p) - get(a-b,p);
}
vector<int> res;// 记录答案,初始值为 1
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]);
return 0;
}
5.满足条件的01序列
#include <iostream>
using namespace std;
typedef long long LL;
const int mod=1e9+7;
// a^k % p
LL qmi(LL a,int k,int p){
LL res=1;
// 对k进行二进制化,从低位到高位
while (k){
// 如果k的二进制表示的第0位为1,则乘上当前的a
// b&1,若为1,b&1=true,反之b&1=false
if(k & 1) res = res * a % p;
// k右移一位
k = k >> 1;
// 更新a,a依次为a^{2^0},a^{2^1},a^{2^2},....,a^{2^logb}
a = a * a % p;
}
return res;
}
int main(){
int n;
cin >> n;
int a = 2 * n,b = n;
int res=1;
// 计算 C(2n,n)
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;
// 最后除以 n+1
res = (LL)res * qmi(n+1,mod-2,mod) % mod;
cout << res << endl;
return 0;
}
九.容斥原理
1.能被整除的数
#include <iostream>
using namespace std;
typedef long long LL;
const int N=20;
int n,m;
int p[N];// 存储 m 个质数
int main(){
cin >> n >> m;
for(int i=0;i<m;i++) cin >> p[i];
int res=0;
// 枚举从1 到 1111...(m个1)的每一个集合状态, (至少选中一个集合)
for(int i=1;i< 1 << m;i++){
// t 表示当前质数乘积,cnt 表示 i 包含几个 1
int t=1,cnt=0;
// 枚举当前状态的每一位
for(int j=0;j<m;j++){
// 当前这一位是 1
if(i >> j & 1){
cnt++;
// 乘积大于n, 则n/t = 0, 跳出这轮循环
if((LL)t * p[j] > n){
t=-1;
break;
}
t *= p[j];// 累乘质数
}
}
if(t != -1){
// 判断 cnt 是奇数还是偶数,奇数加偶数减
// n/t 代表选出的质数的积所对应的集合中在n以内t的倍数有几个
if(cnt % 2 != 0) res += n/t;
else res -= n/t;
}
}
cout << res << endl;
return 0;
}
十.博弈论
1.Nim游戏
#include <iostream>
using namespace std;
int main(){
int n;
int res=0;
scanf("%d",&n);
while (n--){
int x;
cin >> x;
res ^= x;// 依次进行异或操作
}
// 异或值不为0,则先手必胜
// 异或值为0,则先手必败
if(res) puts("Yes");
else puts("No");
return 0;
}
2.台阶-Nim游戏
3.集合-Nim游戏
#include <iostream>
#include <cstring>
#include <unordered_set>
#include <algorithm>
using namespace std;
const int N=110,M=10010;
// m:可以取石子的方案数.如:一次既可以取2个又可取3个算两种方案
// n:有几个石子堆
int n,m;
// s:存放取石子方案,即一次可以取几个石子
// f:1.x个石子的石子堆,算过sg值了吗 2.x个石子的石子堆,sg值是多少
int s[N],f[M];
// 求x个石子的这一堆sg值是多少
int sg(int x){
//若两个石子堆中石子数一样,则它们的SG值也相同,直接返回之间计算过的结果即可
if (f[x] != -1) return f[x];
unordered_set<int> S;// S存放x各个后继节点的sg值
// m种取石子方案一一遍历
for(int i=0;i<m;i++){
int sum = s[i];
// 若有剩余,继续从剩下的x-sum个石子里取.并把后继结点sg值存入S
if(x >= sum) S.insert(sg(x-sum));
}
for(int i=0; ;i++){
// 对S求mex,取集合中不存在的最小自然数
if(!S.count(i)){
f[x] = i;
return f[x];
}
}
}
int main(){
cin >> m;// 限定可以取石子的方案有多少种
for(int i=0;i<m;i++) cin >> s[i];// 输入一次都可以取几个石子
cin >> n;// 一共n个石子堆
// 初始化为-1,即x个石子的石子堆sg值未知
memset(f,-1,sizeof f);
int res=0;
for(int i=0;i<n;i++){
int x;// n个石子堆里各有x个石子
cin >> x;
res ^= sg(x);// 一直进行异或计算
}
// 这n堆石子的sg值异或起来若不为0则先手必胜
if(res != 0) puts("Yes");
else puts("No");
return 0;
}
4.拆分-Nim游戏
(五)第五章-动态规划
一.背包问题
1.01背包
二维
#include <iostream>
using namespace std;
const int N=1010;
int n,V;// n 件物品,背包容积为 V
int v[N],w[N];// 各件物品的体积和价值
int f[N][N];// f[i][j]: j体积下前i个物品的最大价值
int main(){
cin >> n >> V;
for(int i=1;i<=n;i++) cin >> v[i] >> w[i];
for(int i=1;i<=n;i++){
for(int j=1;j<=V;j++){
// 不包含物品i的情况一定存在
f[i][j] = f[i-1][j];
// 包含物品i需要背包体积大于物品i体积
// 根据价值大小,进行决策是否选择第i个物品
if(j >= v[i]){
f[i][j] = max(f[i][j],f[i-1][j-v[i]] + w[i]);
}
}
}
cout << f[n][V] << endl;
return 0;
}
一维(优化空间) !!!若用到上一层的状态时,从大到小枚举, 反之从小到大 !!!
#include <iostream>
using namespace std;
const int N=1010;
int n,V;// n 件物品,背包容积为 V
int v[N],w[N];// 各件物品的体积和价值
int f[N];// f[j]: j体积下物品的最大价值
int main(){
cin >> n >> V;
for(int i=1;i<=n;i++) cin >> v[i] >> w[i];
for(int i=1;i<=n;i++){
// 从大到小枚举枚举,才能保证两式等价,为f[i-1][j]
for(int j=V;j>=v[i];j--){
// f[i][j] = f[i-1][j] i-1为前状态,i为后状态,两式等价
// f[j] = f[j]; 可省略,右边为前状态,左边为后状态
f[j] = max(f[j],f[j-v[i]] + w[i]);
// f[i][j] = max(f[i][j],f[i-1][j-v[i]] + w[i]);
// 倒过来枚举就可以先算f[j],后算f[j-v[i]]
// 即括号内左值为前状态,右值为后状态
// 倒序使得利用上层的数据并没有被污染
}
}
cout << f[V] << endl;
return 0;
}
2.完全背包
二维
#include <iostream>
using namespace std;
const int N=1010;
int n,V;// n 件物品,背包容积为 V
int v[N],w[N];// 各件物品的体积和价值
int f[N][N];// f[i][j]: j体积下前i个物品的最大价值
int main(){
cin >> n >> V;
for(int i=1;i<=n;i++) cin >> v[i] >> w[i];
for(int i=1;i<=n;i++){
for(int j=1;j<=V;j++){
// 取0个物品i的情况
f[i][j] = f[i-1][j];
if(j >= v[i]){
// 注意这里是 f[i][j-v[i]];
f[i][j] = max(f[i][j],f[i][j-v[i]] + w[i]);
}
}
}
cout << f[n][V] << endl;
return 0;
}
一维
#include <iostream>
using namespace std;
const int N=1010;
int n,V;
int v[N],w[N];
int f[N];
/*
*1.01背包: f[i][j] = max(f[i][j],f[i-1][j-v[i]] + w[i]);
*2.完全背包:f[i][j] = max(f[i][j],f[i][j-v[i]] + w[i]);
*/
int main(){
cin >> n >> V;
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<=V;j++){
// 取0个物品i的情况
// f[i][j] = f[i-1][j] 左边后状态,右边前状态
f[j] = f[j]; //可省略
// 注意这里是 f[i][j-v[i]];
f[j] = max(f[j],f[j-v[i]] + w[i]);
// f[i][j] = max(f[i][j],f[i][j-v[i]] + w[i]);
// f[j-v[i]] 比 f[j] 先算出来,因此就是 i 层的 f[j-v[i]]
// 由于并没有用到上层数据,因此不用逆序
}
}
cout << f[V] << endl;
return 0;
}
3.多重背包
二维
#include <iostream>
using namespace std;
const int N=110;
int n,V;
int v[N],w[N],s[N];// s[i] 为每件物品个个数
int f[N][N];
int main(){
cin >> n >> V;
for(int i=1;i<=n;i++) cin >> v[i] >> w[i] >> s[i];
for(int i=1;i<=n;i++){
for(int j=1;j<=V;j++){
// 物品i个数要小于s[i],且其体积和小于背包体积
for(int k=0;k<=s[i] && k*v[i]<=j;k++){
// 选择 k 个物品 i,每次循环包含了选与不选的情况
f[i][j] = max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
}
}
}
cout << f[n][V] << endl;
return 0;
}
一维
#include <iostream>
using namespace std;
const int N=110;
int n,V;
int v[N],w[N],s[N];// s[i] 为每件物品个个数
int f[N];
int main(){
cin >> n >> V;
for(int i=1;i<=n;i++) cin >> v[i] >> w[i] >> s[i];
for(int i=1;i<=n;i++){
for(int j=V;j>=v[i];j++){
for(int k=0;k<=s[i] && k*v[i]<=j;k++){
// f[i][j] = max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
f[j] = max(f[j],f[j-k*v[i]]+k*w[i]);
// j 逆序枚举,f[j] 比 f[j-k*v[i]] 先计算出来
// 用到上层数据,因此逆序
}
}
}
cout << f[V] << endl;
return 0;
}
4.多重背包II
一维
#include <iostream>
using namespace std;
// logs(每个物品最多 s 个)= log2000 : 约等于11,所以打包11组
const int N=11 * 1000 + 10,M=2010;
int n,m;// 物品种类,背包容积
int V[N],W[N];// 物品体积,价值
int f[M];// f[j]: j体积下最大价值
int main(){
cin >> n >> m;
int cnt=0;// 将物品重新分组后的顺序
for(int i=1;i<=n;i++){
int v,w,s;// 体积,价值,个数
cin >> v >> w >> s;
// 二进制拆分 打包时每组中有 k 个同种物品
int k=1;
while (k<=s){
cnt++;// 编号增加
V[cnt] = v * k;// 每组的体积
W[cnt] = w * k;// 每组的价值
s -= k;// 从总个数s中减去k个
k *= 2;// k * 2,每次增长一倍
}
// 二进制拆分完之后 剩下的物品个数分为新的一组
if(s > 0){
cnt++;
V[cnt] = v * s;
W[cnt] = w * s;
}
}
// 关键一步!将物品编号更改为所有物品在箱子里装完后箱子的编号
n = cnt;
// 转化为01背包问题
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]);
}
}
cout << f[m] << endl;
return 0;
}
5.分组背包
#include <iostream>
using namespace std;
const int N=110;
int n,V;
int v[N][N],w[N][N],s[N];// v/w[i][j]:第 i 组第 j 个的体积/价值
int f[N];
int main(){
cin >> n >> V;
for(int i=1;i<=n;i++){
cin >> s[i];// 每组数量
for(int j=0;j<s[i];j++){
cin >> v[i][j] >> w[i][j];
}
}
for(int i=1;i<=n;i++){
for(int j=V;j>=1;j--){
for(int k=0;k<s[i];k++){// 枚举所有选择
if(v[i][k] <= j){// 满足体积小于背包体积
// 是否选择第 i 组第 j 个物品
// 用到前状态,因此枚举体积倒序
f[j] = max(f[j],f[j-v[i][k]] + w[i][k]);
}
}
}
}
cout << f[V] << endl;
return 0;
}
二.线性DP
1.数字三角形
#include <iostream>
using namespace std;
const int N=510,INF=1e9;
int n;
int a[N][N];// 存储每个点
int f[N][N];// f[i][j]: (1,1)走到(i,j)路径最大值
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
scanf("%d",&a[i][j]);
}
}
for(int i=1;i<=n;i++){
// 注意这里j从 0 到 i+1
// 三角形左边的点没有左上路径,右边的点没有右上路径
for(int j=0;j<=i+1;j++){
f[i][j] = -INF;// 初始化为负无穷
}
}
f[1][1] = a[1][1];
for(int i=2;i<=n;i++){// 从第二层开始遍历
for(int j=1;j<=i;j++){
// 左上和右上两种路径取max
f[i][j] = max(f[i-1][j-1] + a[i][j],f[i-1][j] + a[i][j]);
}
}
int res=-INF;// 找到到达最下层的路径最大值
for(int i=1;i<=n;i++) res = max(res,f[n][i]);
cout << res << endl;
return 0;
}
自上而下简化版
#include <iostream>
using namespace std;
const int N=510,INF=1e9;
int n;
int a[N][N];// 存储到(i,j)最长路径
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
// 注意这里j从 0 到 i+1
// 三角形左边的点没有左上路径,右边的点没有右上路径
for(int j=0;j<=i+1;j++){
a[i][j] = -INF;// 初始化为负无穷
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
scanf("%d",&a[i][j]);
// 一边输入,一边在两条路径中取最大值
a[i][j] += max(a[i-1][j-1],a[i-1][j]);
}
}
int res=-INF;// 找到到达最下层的路径最大值
for(int i=1;i<=n;i++) res = max(res,a[n][i]);
cout << res << endl;
return 0;
}
自下而上简化版
#include <iostream>
using namespace std;
const int N=510;
int n;
int a[N][N];
int main(){
scanf("%d",&n);
// 这种写法的好处是不用考虑边界问题,不用设置初始值为负无穷
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
scanf("%d",&a[i][j]);
}
}
// 自下而上寻找路径
for(int i=n;i>=1;i--){
for(int j=1;j<=i;j++){
a[i][j] += max(a[i+1][j+1],a[i+1][j]);
}
}
// 到达(1,1)路径即为最大值
cout << a[1][1] << endl;
return 0;
}
2.最长上升子序列
O(n^2)
#include <iostream>
using namespace std;
const int N=1010;
int n;
int a[N];// 存储序列
int f[N];// f[i]: 以 i 结尾的最长递增序列长度
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++){
f[i] = 1;// 只有 a[i] 一个数
// 枚举上一个数,比较 该路径 与 上一个数路径+1 的长度
for(int j=1;j<i;j++){
if(a[j] < a[i]){// 前提是保证递增序列
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 << endl;
return 0;
}
输出路径 (存储状态转移)
#include <iostream>
using namespace std;
const int N=1010;
int n;
int a[N];// 存储序列
int f[N];// f[i]: 以 i 结尾的最长递增序列长度
int path[N];// g[i]: 记录 i 是由哪个状态转移来的
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++){
f[i] = 1;
path[i] = 0;
for(int j=1;j<i;j++){
if(a[j] < a[i]){
// f[i] = max(f[i],f[j] + 1);
if(f[i] < f[j] + 1){
f[i] = f[j] + 1;
// 只有在更新路径情况下,才有前状态
path[i] = j;
}
}
}
}
int k = 1;// 找到最大路径的结尾所在点
for(int i=1;i<=n;i++){
if(f[i] > f[k]) k = i;
}
cout << f[k] << endl;
// 注意这里是逆序输出
for(int i=0,len=f[k];i<len;i++){
cout << a[k] << ' ';// 输出结尾点
k = path[k];// 找到前状态点
}
return 0;
}
3.最长上升子序列II
4.最长公共子序列
#include <iostream>
using namespace std;
const int N=1010;
int n,m;
char a[N],b[N];// 两个字符串
// f[i][j]: 第一个字符串前i个字符,第二个字符串前j个字符中出现
int f[N][N];
int main(){
scanf("%d%d",&n,&m);
scanf("%s%s",a+1,b+1);// 下标从1开始读入
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
// 先取中间两种情况max
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);
}
}
}
printf("%d\n",f[n][m]);
return 0;
}
三.区间DP
1.石子合并
#include <iostream>
using namespace std;
const int N=310;
int n;
int a[N],sum[N];// sum[]计算前缀和
int f[N][N];// f[i][j]: 区间i-j的最小代价
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++) {
scanf("%d", &a[i]);// 每堆石子个数
sum[i] = sum[i - 1] + a[i];// 计算前缀和
}
for(int len=1;len <= n;len++){// 枚举区间长度
for(int l=1;l+len <= n;l++){// 枚举左端点
int r = l+len;// 通过长度计算右端点
f[l][r] = 1e8;// 要先初始化为无穷,方便找最小值
// 枚举每种合并方式,也就是枚举l-r之间的断点,k+1必须 <= r
for(int k=l;k <= r-1;k++){
// 状态转移方程
f[l][r] = min(f[l][r],f[l][k] + f[k+1][r] + sum[r] - sum[l-1]);
}
}
}
printf("%d\n",f[1][n]);
return 0;
}
四.计数类DP
1.整数划分
#include <iostream>
using namespace std;
const int N=1010,mod=1e9+7;
int n;// 被拆分的数,可以看作是背包体积
int f[N][N];// f[i][j]: 前i个整数(1,2…,i)恰好拼成j的方案数
// 把数字 i 的体积看作 i,即转化为完全背包问题
int main(){
cin >> n;
// 这里初始化和完全背包不同
// 构成 0 的方案数为 1
f[0][0] = 1;
for(int i=1;i<=n;i++){
for(int j=0;j<=n;j++) {
// 取0个物品i的情况
f[i][j] = f[i - 1][j] % mod;
if (j >= i) {
f[i][j] = (f[i - 1][j] + f[i][j - i]) % mod;
}
}
}
// 一维做法:
/*
f[0] = 1;
for(int i = 1 ; i <= n ; i++ )
for(int j = 0 ; j <= n ; j++ )
f[j] = (f[j] + f[j-i])%mod;
cout << f[n] << endl;
*/
cout << f[n][n] << endl;
return 0;
}
五.数位统计DP
1.计数问题
六.状态压缩DP
1.蒙德里安的理想
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
const int N = 11,M = 1 >> N;// M = 2^N
int n,m;
int f[N][M];// 第一维表示列, 第二维表示所有可能的状态
vector<int> state[M];// 二维数组
bool st[N];// 存储每种状态是否有奇数个连续的0,如果奇数个0是无效状态,如果是偶数个零置为true
七.树形DP
1.没有上司的舞会
#include <iostream>
#include <cstring>
using namespace std;
const int N=6010;
int n;
int happy[N];// 每个职工高兴度
int h[N],e[N],ne[N],idx;// 领接表存储
int f[N][2];// 结点状态,0 为不选,1 为选
bool has_father[N];// 判断是否有父结点
void add(int a,int b){
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
void dfs(int u){
// 如果选当前节点u,就可以把f[u,1]先怼上他的高兴度
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(){
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&happy[i]);
memset(h,-1,sizeof h);
for(int i=1;i<n;i++){
int a,b;
scanf("%d%d",&a,&b);// b 是 a 的上司
add(b,a);
has_father[a] = true;// 有父节点
}
// 寻找根节点
int root = 1;
while (has_father[root]) root++;
// 从根节点开始搜索,输出不选根节点与选根节点的最大值
dfs(root);
printf("%d\n",max(f[root][0],f[root][1]));
return 0;
}
八.记忆化搜索
1.滑雪
#include <iostream>
#include <cstring>
using namespace std;
const int N=310;
int n,m;// 网格滑雪场的行和列
int h[N][N];// 网格滑雪场各点高度
int f[N][N];// f[i][j]: 从(i,j)开始滑雪的最远距离
int dx[4] = {-1,0,1,0},dy[4] = {0,1,0,-1};
int dp(int x,int y){
int &v = f[x][y];// 简化计算,后面 v 即是 f[x][y]
if(v != -1) return v;// 如果已经计算过了,就可以直接返回答案
v=1;// 路径长度至少为 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[a][b] < h[x][y]){
// (原距离) 与 (下个点开始dp的路径长度+1) 比较
v = max(v,dp(a,b)+1);
}
}
return v;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
scanf("%d",&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));
}
}
printf("%d\n",res);
return 0;
}
(六)第六章-贪心
一.区间问题
1.区间选点
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
typedef pair<int,int> PII;
int n;
vector<PII> intervals;// 输入区间
bool cmp(PII a,PII b){
return a.second < b.second;
}
int main(){
cin >> n;
while (n--){
int l,r;
cin >> l >> r;
intervals.push_back({l,r});
}
// 右端点从小到大排序
sort(intervals.begin(),intervals.end(), cmp);
// res是选择区间个数,ed表示上一个选中的区间的右端点
int res = 0,ed = -1e9-10;
for(auto item : intervals){
// 如果当前区间的左端点大于上一个区间的右端点,选择该区间
if(item.first > ed){
res ++;
ed = item.second;// 更新已选区间的右端点
}
}
printf("%d\n",res);
return 0;
}
2.最大不相交区间数量
可以按前一题的方式将所有区间分为几个集合,每个集合中各个区间都至少有一点相交。若要选取不相交两个区间,那么两个区间必定处于不同的集合中,而最大的不相交区间数量便是总集合数,也就是区间选点的数量。所以两题代码相同。
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
typedef pair<int,int> PII;
int n;
vector<PII> intervals;// 输入区间
bool cmp(PII a,PII b){
return a.second < b.second;
}
int main(){
cin >> n;
while (n--){
int l,r;
cin >> l >> r;
intervals.push_back({l,r});
}
// 右端点从小到大排序
sort(intervals.begin(),intervals.end(), cmp);
// res是选择区间个数,ed表示上一个选中的区间的右端点
int res = 0,ed = -1e9-10;
for(auto item : intervals){
// 如果当前区间的左端点大于上一个区间的右端点,选择该区间
if(item.first > ed){
res ++;
ed = item.second;// 更新已选区间的右端点
}
}
printf("%d\n",res);
return 0;
}
3.区间分组
#include <iostream>
#include <algorithm>
#include <vector>
#include <queue>
using namespace std;
typedef pair<int,int> PII;
int n;
vector<PII> intervals;// 输入区间
bool cmp(PII a,PII b){
return a.first < b.first;
}
int main(){
cin >> n;
// 不能写 while(n--),因为后面会用到 n
for(int i=0;i<n;i++){
int l,r;
cin >> l >> r;
intervals.push_back({l,r});
}
// 左端点从小到大排序
sort(intervals.begin(),intervals.end(), cmp);
// 小根堆维护每个组的右端点值最大的区间
priority_queue<int,vector<int>,greater<int>> heap;
for(int i=0;i<n;i++){
auto t = intervals[i];
// 若此区间左端点小于 所有组最大右端点中的最小值
// 则加入新组,即加入小根堆
if(heap.empty() || heap.top() >= t.first){
heap.push(t.second);
} else{
// 否则,说明可以分为1个组,再更新这个组右端点的值
// 即先拿走一个组,更新右端点后再加入堆中
heap.pop();
heap.push(t.second);
}
}
// 小根堆大小即为分组个数
cout << heap.size() << endl;
return 0;
}
4.区间覆盖
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
typedef pair<int,int> PII;
int n;
vector<PII> intervals;// 输入区间
bool cmp(PII a,PII b){
return a.first < b.first;
}
int main(){
int st,ed;
cin >> st >> ed >> n;// st-ed 为目标区间
// 不能写 while(n--),因为后面会用到 n
for(int i=0;i<n;i++){
int l,r;
cin >> l >> r;
intervals.push_back({l,r});
}
// 左端点从小到大排序
sort(intervals.begin(),intervals.end(), cmp);
int res = 0;// 最终用到的区间个数
bool sta = false;
//双指针扫描,i 记录扫描哪个区间
for(int i = 0,j = i; i < n; i = j)
{
int max_r = -2e9;
// 如果区间左端点可以覆盖 st
while(intervals[j].first <= st && j < n){
// 寻找最大右端点所在区间
max_r = max(max_r, intervals[j].second);
j++;// 继续向后找,将要扫描第j个区间(和i=j对应)
}
// 如果最大右端点都无法覆盖st,直接结束
if(max_r < st) break;
else{
res++;// 可以覆盖,则选择该区间
if(max_r >= ed){
sta = true;// 更新到了覆盖 ed 的区间,则成功
break;
}
}
// 如果没有结束,将 st 更新为最大右端点继续寻找
st = max_r;
}
if(sta) cout << res;
else cout << -1;
return 0;
}
二.Huffman树
1.合并果子
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
int main(){
int n;
cin >> n;
priority_queue<int,vector<int>,greater<int>> heap;
while (n--){
int x;
cin >> x;
heap.push(x);// 小根堆存储各堆果子数量
}
int res = 0;
while (heap.size() > 1){
// 取出最小的两堆进行合并
int a = heap.top();heap.pop();
int b = heap.top();heap.pop();
heap.push(a + b);
// 消耗体力为两堆和
res += a + b;
}
cout << res << endl;
return 0;
}
三.排序不等式
1.排队打水
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 100010;
int n;
int t[N];
int main(){
cin >> n;
for(int i=0;i<n;i++) cin >> t[i];
sort(t,t + n);// 从小到大排序
LL res = 0;
// 计算排队总时间
for(int i=0;i<n;i++) res += t[i] * (n - i- 1);
cout << res << endl;
return 0;
}
四.绝对值不等式
1.货仓选址
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
int a[N];
int main(){
cin >> n;
for(int i=0;i<n;i++) cin >> a[i];
sort(a,a + n);// 对所有坐标排序
int res = 0;
// 货仓设在中心位置,计算累加距离
for(int i=0;i<n;i++) res += abs(a[i] - a[n / 2]);
cout << res << endl;
return 0;
}
五.推公式
1.耍杂技的牛
#include <iostream>
#include <algorithm>
using namespace std;
typedef pair<int,int> PII;
const int N = 50010;
int n;
PII cows[N];
int main(){
cin >> n;
for(int i=0;i<n;i++){
int w,s;
cin >> w >> s;
// 第一个这样存储是方便排序,第二个是 w 还是 s 都行
// 是为了可以将 w 和 s 分别提取
cows[i] = {w + s,w};
}
sort(cows,cows + n);// 按 w + s 从小到大排序
// sum 记录这头牛的上方牛的重量和,res 记录最大风险
int sum = 0,res = -2e9;
for(int i=0;i<n;i++){
// 取出这头牛的重量和强壮程度
int w = cows[i].second,s = cows[i].first - w;
// 上面的牛牛们的总体重 - 当前牛的强壮程度
res = max(res,sum - s);
// 下一头牛上面的牛牛们的总体重
sum += w;
}
cout << res << endl;
return 0;
}