
【记录】算法 - 算法基础c++
【代码】算法基础 - c++
一、基础算法
(1)排序
快速排序
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);// 找到第一个>=x的数
do j--; while(q[j] > x);// 找到第一个<=x的数
if(i < j) swap(q[i], q[j]);
}
// (l, i)为<=x的数,(j, r)为>=x的数
// quick_sort(q, l, i - 1), quick_sort(q, i, r);// 注意x=q[l]存在边界问题,死循环
quick_sort(q, l, j), quick_sort(q, j+1, r);// 注意x=q[r]存在边界问题,死循环
}
// AcWing 786. 第k个数
// 用快排思想划分
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int a[N];
int n, k;
int quick_sort(int l, int r, int k){
if(l >= r) return a[l];
int i = l - 1, j = r + 1, x = a[l];
while(i < j){
do i ++; while(a[i] < x);
do j --; while(a[j] > x);
if(i < j) swap(a[i], a[j]);
}
int sl = j - l + 1;// <=x的元素个数
if(sl >= k) return quick_sort(l, j, k);
return quick_sort(j+1, r, k-sl);
}// 时间复杂度O(n),n+n/2+n/4+....
int main(){
scanf("%d%d", &n, &k);
for(int i = 0; i < n; i++) scanf("%d", &a[i]);
printf("%d", quick_sort(0, n-1, k));
return 0;
}
归并排序
void merge_sort(int q[], int l, int r){
if(l >= r) return;
int m = l + r >> 1;
merge_sort(q, l, m), merge_sort(q, m+1, r);
int k = 0, i = l, j = m + 1; // 合并
while(i <= m && j <= r)
if(q[i] <= q[j]) temp[k++] = q[i++];
else temp[k++] = q[j++];
while(i <= m) temp[k++] = q[i++];
while(j <= r) temp[k++] = q[j++];
for(i = l, j = 0; i <= r; i++, j++) q[i] = temp[j];
}
// AcWing 788. 逆序对的数量
// 一边归并排序,一边计算逆序对数量
#include <iostream>
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
int q[N], tmp[N];
int n;
ll merge_sort(int l, int r){
if(l >= r) return 0;
int m = l + r >> 1;
ll res = merge_sort(l, m) + merge_sort(m+1, r);// 逆序对只在左边or只在右边
int k = 0, i = l, j = m + 1;
while(i <= m && j <= r){
if(q[i] <= q[j]) tmp[k++] = q[i++];
else{
tmp[k++] = q[j++];
res += m - i + 1;// 逆序对一个在左,一个在右;与当前j构成的逆序对数量=i~m的个数(qi~qm都>qj)
}
}
while(i <= m) tmp[k++] = q[i++];
while(j <= r) tmp[k++] = q[j++];
for(int i = l, j = 0; i <= r; i++, j++) q[i] = tmp[j];
return res;
}
int main(){
cin >> n;
for(int i = 0; i < n; i ++) cin >> q[i];
cout << merge_sort(0, n-1);
return 0;
}
堆排序
// 常见堆操作
void down(int u){
int t = u;
// 当左孩子存在,且左孩子比u还小
if(u*2 <= cnt && h[u*2] < h[t]) t = u * 2;
// 当右孩子存在,且右孩子比t还小
if(u*2+1 <= cnt && h[u*2+1] < h[t]) t = u * 2 + 1;
// 最小的不是节点u
if(t != u){
swap(h[u], h[t]);
down(t);
}
}
void up(int u){
// 当爹存在,且u比爹小
while(u/2 && h[u/2] > h[u]){
swap(h[u/2], h[u]);
u /= 2;
}
}
// Acwing 838. 堆排序
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int h[N];// 小根堆,从下标为1开始存储
int n, m, cnt;// cnt记录堆的大小
void down(int u){
int t = u;
// 当左孩子存在,且左孩子比u还小
if(u*2 <= cnt && h[u*2] < h[t]) t = u * 2;
// 当右孩子存在,且右孩子比t还小
if(u*2+1 <= cnt && h[u*2+1] < h[t]) t = u * 2 + 1;
// 最小的不是节点u
if(t != u){
swap(h[u], h[t]);
down(t);
}
}
int main(){
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++) scanf("%d", &h[i]);
cnt = n;
// 从n/2的地方(倒数第二层)开始down,时间复杂度降到O(n)
for(int i = n/2; i; i--) down(i);
while(m--){
printf("%d ", h[1]);
h[1] = h[cnt--];
down(1);
}
return 0;
}
// Acwing 839. 模拟堆
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int h[N];// 值,小根堆
int ph[N];// ph[k] = x,第k个插入的元素在堆中存放的位置x
int hp[N];// hp[x] = k,堆中存放的位置x是第k个插入的
int cnt = 0, cnt_i = 0;
void heap_swap(int x, int y){
swap(ph[hp[x]], ph[hp[y]]);
swap(hp[x], hp[y]);
swap(h[x], h[y]);
}
void up(int u){
while(u/2 && h[u/2] > h[u]){
heap_swap(u/2, u);
u /= 2;
}
}
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(t != u){
heap_swap(t, u);
down(t);
}
}
int main(){
int n, k, x; cin >> n;
string op;
while(n--){
cin >> op;
if(op == "I"){
cin >> x;
h[++cnt] = x;
ph[++cnt_i] = cnt;
hp[cnt] = cnt_i;
up(cnt);
}else if(op == "PM"){
cout << h[1] << endl;
}else if(op == "DM"){
heap_swap(1, cnt--);
down(1);
}else if(op == "D"){
cin >> k;
x = ph[k];
heap_swap(ph[k], cnt--);
up(x);
down(x);
}else{
cin >> k >> x;
h[ph[k]] = x;
up(ph[k]);
down(ph[k]);
}
}
return 0;
}
(2)二分
看到最大(小)…里的最小(大),就要想起二分!
整数二分
// 找右边界(1、红色部分)
int bsearch1(int l, int r){
while(l < r){
int mid = (l + r + 1) >> 1;// +1防止死循环
if(check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
// 找左边界(2、绿色部分)
int bsearch2(int l, int r){
while(l < r){
int mid = (l + r) >> 1;
if(check(mid)) r = mid;
else l = mid + 1;
}
return l;
}
浮点数二分
double bsearch(double l, double r){
const double eps = 1e-8;// 题目要求n位小数则多精确两位
while(r - l > eps){
double mid = (l + r) / 2;
if(check(mid)) r = mid;// 不用考虑边界
else l = mid;
}
return l;
}
// AcWing 790. 数的三次方根
#include <iostream>
using namespace std;
int main(){
double x; cin >> x;
double l = -10000, r = 10000;
while(r - l > 1e-8){
double m = (l + r) / 2;
if(m * m * m >= x) r = m;
else l = m;
}
printf("%lf\n", l);
return 0;
}
(3)高精度
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];
if(i < B.size()) t += B[i];
c.push_back(t % 10);
t /= 10;
}
if(t) c.push_back(1);
return c;
}
A-B
// 比较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;
}
// 默认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;
if(i < B.size()) t -= B[i];
c.push_back((t+10)%10);
t = t < 0 ? 1 : 0;// 借位
}
while(c.size() > 1 && c.back() == 0) c.pop_back();// 去掉前导0
return c;
}
Axb
vector<int> mul(vector<int> &A, int b){
vetcor<int> c;
for(int i = 0, t = 0; i < A.size() || t; i ++){ // 最后t进位
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;
}
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();
return c;
}
(4)前缀和与差分
一维前缀和
S(l-1) = a1 + a2 + … + a(l-1)
Sr = a1 + a2 + … + a(l-1) + al + … + ar
区间和S[l, r] = Sr - S(l-1)
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int n, m;
int a[N], s[N];
int main(){
// 数据>1e6用scanf,否则cin
// ios::sync_with_stdio(false);// 取消cin同步,加快cin读取速度,但不能使用scanf
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++) scanf("%d", &a[i]); // 其实没必要存a[i]
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;
}
二维前缀和
// AcWing 796. 子矩阵的和
#include <iostream>
using namespace std;
const int N = 1010;
int n, m, q;
int a[N][N], 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", &a[i][j]); // 其实没必要存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;
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;
}
一维差分
a1, a2, … , ai
构造ai = b1 + b2 + … + bi,数组b为a的差分,数组a为b的前缀和。
// AcWing 797. 差分
#include <iostream>
const int N = 1e5 + 10;
int n, m;
int a[N], b[N];// a前缀和,b差分
// 使得a[l]+c, ..., a[r]+c
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]); // 可以合并操作
// 初始化差分,相当于在[i,i]使得a[i] += c
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;
}
二维差分
// AcWing 798. 差分矩阵
#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;
scanf("%d%d%d%d%d", &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)双指针
把O(n^2)的朴素算法优化到O(n)
for(i = 0, j = 0; i < n; i++){
while(j < i && check(i, j)) j++;
...
}
// AcWing 799. 最长连续不重复子序列
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int n;
int a[N];
int s[N];// s[i]表示i出现的次数
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++){
s[a[i]] ++;// a[i]加入序列
while(s[a[i]] > 1){// j~i序列里有重复元素
s[a[j]]--;
j++;// j向前移动
}
res = max(res, i - j + 1);
}
cout << res << endl;
return 0;
}
(6)位运算
n >> k & 1
- 表示n的二进制表示第k位(从右往左)上的数字
lowbit(n)
- = n & -n(n&n的补码,-n=~n+1)
- 返回n的二进制表示最右一个1(1010返回0010)
// AcWing 801. 二进制中1的个数
#include <iostream>
using namespace std;
int n;
int lowbit(int x){
return x & -x;
}
int main(){
cin >> n;
for(int i = 1; i <= n; i++){
int x; cin >> x;
int cnt = 0;
while(x) x -= lowbit(x), cnt ++;
cout << cnt << " ";
}
return 0;
}
(7)离散化
数据值域跨度大,但用到的不多
// 存储所有待离散的值
vector<int> alls;
// 排序
sort(alls.begin(), alls.end());
// 去除重复元素
alls.erase(unique(alls.begin(), alls.end()), alls.end());
// 二分找出x对应的离散化的值(返回第一个>=x的位置)
int find(int x){
int l = 0, r = alls.size()-1;
while(l < r){
int m = l + r >> 1;
if(alls[m] >= x) r = mid;
else l = mid + 1;
}
return r + 1;// 映射到1, 2, ...n
}
// AcWing 802. 区间和
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef pair<int, int> PII;
const int N = 300000 + 10;
// 把插入and查询操作的下标alls 离散化处理映射到 a,不离散会超时,因为要开2*10^9大小的数组
int a[N], s[N];// 数(映射后,总大小为n+2m=300000),前缀和
vector<int> alls;// 所有的插入和查询操作的下标(待映射的n+2m个坐标)
vector<PII> add, query;// 插入操作,查询操作
int n, m;
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;// 使用前缀和,下标+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);
}
// 去重
sort(alls.begin(), alls.end());
alls.erase(unique(alls.begin(), alls.end()), alls.end());
// 插入
for(auto item : add){
int x = find(item.first);// 找到去重后映射的下标
a[x] += item.second;
}
// 处理前缀和,这里的alls.size()是去重后的
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)区间合并
void merge(vector<PII> &segs){
vector<PII> res;
sort(segs.begin(), segs.end());
int st = -2e9, ed = -2e9;
for(auto seg : segs)
if(seg.first > ed){
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;
}
// AcWing 803. 区间合并
// 类似会议安排
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef pair<int, int> PII;
vector<PII> segs;
int n;
int main(){
cin >> n;
while(n--){
int l, r;
cin >> l >> r;
segs.push_back({l, r});
}
// pair排序默认以pair.first
sort(segs.begin(), segs.end());
int cnt = 0, ed = -2e9;
for(auto item : segs){// 不需要知道区间的具体值,所以只需要更新ed
if(item.first > ed) cnt ++;
ed = max(ed, item.second);
}
cout << cnt << endl;
return 0;
}
(9)递归
// 汉诺塔问题
#include <iostream>
using namespace std;
void move(int n, char from, char to){ // 把编号为n的盘子从from移到to
printf("%d: %c -> %c\n", n, from, to);
}
void hanoi(int n, char A, char B, char C){ // 把n个盘子从A移到C,B是辅助盘子
if(n == 1){
move(n, A, C);
return;
}
hanoi(n-1, A, C, B); // 把上面n-1个盘子从A移到B
move(n, A, C); // 把最底下的盘子从A移到C
hanoi(n-1, B, A, C); // 把n-1个盘子从B移到C
}
int main(){
int n; cin >> n;
hanoi(n, 'A', 'B', 'C');
}
二、数据结构
(1)链表与邻接表
单链表
用静态链表模拟单链表
// AcWing 826. 单链表
#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--){
char op;
int k, x;
cin >> op;
if(op == 'H'){
cin >> x;
add_to_head(x);
}else if(op == 'D'){
cin >> k;
if(k) remove(k-1);
else head = ne[head];// 删除头节点
}else{
cin >> k >> x;
add(k-1, x);
}
}
for(int i = head; i != -1; i = ne[i]) cout << e[i] << ' ';
return 0;
}
双链表
// AcWing 827. 双链表
#include <iostream>
using namespace std;
const int N = 100010;
int m;
int e[N], l[N], r[N], idx;
void init(){
// 下标为0的点做左边界点,下标为1的点做右边界点
r[0] = 1; l[1] = 0;
// 初始idx从2开始
idx = 2;
}
// 将x插入到下标为a的点右边
void insert(int a, int x){
e[idx] = x;
l[idx] = a, r[idx] = r[a];
l[r[a]] = idx, r[a] = idx ++;
}
// 删除下标为a的点
void remove(int a){
l[r[a]] = l[a];
r[l[a]] = r[a];
}
int main(){
cin >> m;
init();
while(m--){
string op;cin >> op;
int k, x;
if(op == "L"){
cin >> x;
insert(0, x);
}else if(op == "R"){
cin >> x;
insert(l[1], x);
}else if(op == "D"){
cin >> k;
remove(k+1);
}else if(op == "IL"){
cin >> k >> x;
insert(l[k+1], x);
}else{
cin >> k >> x;
insert(k+1, x);
}
}
for(int i = r[0]; i != 1; i = r[i]) cout << e[i] << " ";
return 0;
}
邻接表
见树与图
(2)栈与队列
栈
用数组模拟栈
// tt栈顶
int stk[N], tt = -1;
// 插入x
stk[++t] = x;
// 弹出栈顶
tt--;
// 栈顶的值
stk[tt];
// 判断栈是否为空
if(tt == -1) return true;
else false;
// 判断栈是否为满
if(tt == N-1) return true;
else false;
// 栈的长度
tt + 1;
队列
用数组模拟队列
// 普通队列
// hh队头,tt队尾
int q[N], hh = 0, tt = -1;
// 插入
q[++tt] = x;
// 队头出队
hh++;
// 队头的值
q[hh];
// 判断队列是否为空
if(hh <= tt) return false;
else return true;
// 循环队列
// hh队头,tt队尾的下一个位置
int q[N], hh = 0, tt = 0;
// 插入
q[tt] = x;
tt = (tt + 1) % N;
// 从队头弹出
hh = (hh + 1) % N;
// 队头的值
q[hh];
// 判断队列是否为空
if(hh == tt) return true;
else return false;
// 判断队列是否为满(以队头在队尾指针的下一位作为标志)
if((tt+1)%N == hh) return true;
else return false;
// 队列长度
(hh - tt + N) % N
单调栈
// AcWing 830. 单调栈
// 输出每个数左边第一个比它小的数
#include <iostream>
using namespace std;
const int N = 100010;
int n;
int stk[N], tt;
int main(){
cin >> n;
while(n--){
int x; cin >> x;
// 当栈不为空and栈顶元素>=x,栈顶弹出(保持栈顶为最近的最小值)
while(tt && stk[tt] >= x) tt--;
if(tt) cout << stk[tt] << " ";
else cout << "-1 ";
// 当前x入栈
stk[++tt] = x;
}
return 0;
}
单调队列
// AcWing 154. 滑动窗口
#include <iostream>
using namespace std;
const int N = 1e6+10;
int a[N], q[N];// a保存数值;q队列模拟滑动窗口,保存下标
int hh = 0, tt = -1;
int n, k;
int main(){
scanf("%d%d", &n, &k);
for(int i = 0; i < n; i++) scanf("%d", &a[i]);
// 找滑动窗口的最小值
for(int i = 0; i < n; i++){
// 队头元素不在窗口内(队列非空&&队头在数组中的下标<当前窗口的最小下标),队头出队
// 用if是因为每次只有一个元素进队,一个元素出队
if(hh <= tt && q[hh] < i-k+1) hh++;
// 队列非空&&队尾>=当前元素,队尾出队(保持队列单调递增,队头为当前窗口最小值)
while(hh <= tt && a[q[tt]] >= a[i]) tt--;
// 当前i入队
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[q[tt]] <= a[i]) tt--;
q[++tt] = i;
if(i >= k - 1) printf("%d ", a[q[hh]]);
}
return 0;
}
(3)kmp
// 朴素模式匹配
// 时间复杂度O(nm)
void search(string p, string s){
// 模式串p,字符串s
// 从下标1开始匹配
// i指向字符串,j指向模式串,两个一起移动
for(int i = 1; i <= s.size(); i++){
bool flag = true;
for(int j = 1; j <= p.size(); j++){
if(s[i+j-1] != p[j]){// 注意是i + j-1,不是j
flag = false;
break;
}
}
}
}
// kmp
// 时间复杂度O(n+m)
// 利用已知s串后缀匹配p串的情况,减少s串在匹配p串时存在的回溯的情况
#include <iostream>
using namespace std;
const int N = 1e5+10, M = 1e6+10;
char p[N], s[M];
int ne[N];
int n, m;
int main(){
// p和s从下标1开始存储
cin >> n >> p+1 >> m >> s+1;
// 求next数组
// i从2开始,因为ne[1]恒=0;ne[i]=j表示模式串p的[i-j+1~i]=[1~j](后缀=前缀),即从模式串p的i位置开始,最大匹配长度j
for(int i = 2, j = 0; i <= n; i++){
while(j && p[i] != p[j+1]) j = ne[j]; // 不断尝试扩展匹配长度j,如果扩展失败,回溯j=next[j],直到j=0
if(p[i] == p[j+1]) j++; // 如果匹配成功,j长度+1
ne[i] = j;
}
// 匹配字符串
// i从1开始,j从0开始
for(int i = 1, j = 0; i <= m; i++){
// s[i]和p[j+1]是否匹配,不匹配,j递归=ne[j],不断地使s串的后缀=p串的前缀,直到s[i]=p[j+1]或者j=0
while(j && s[i] != p[j+1]) j = ne[j];
// 匹配,j往后移一位
if(s[i] == p[j+1]) j++;
// 完全匹配
if(j == n) printf("%d ", i-n);
}
return 0;
}
(4)前缀树Trie
// AcWing 835.Trie字符串统计
#include <iostream>
using namespace std;
const int N = 1e5 + 10;// 字符串长度
// son[x][y]=z表示节点x第y个儿子下标为z
// cnt[]表示以该节点结尾的单词有几个
// idx表示当前用到哪个节点
int son[N][26], cnt[N], idx;
int n;
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];
}
return cnt[p];
}
int main(){
cin >> n;
while(n--){
char op;
cin >> op >> str;
if(op == 'I') insert(str);
else cout << query(str) << endl;
// 如果是用scanf,建议用%s配合char[]读;用%c会把前一行的换行、空格读入
/*
char op[2];
scanf("%s%s", op, str);
if(*op == "I") ...
*/
}
return 0;
}
// AcWing 143. 最大异或对
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10, M = 31 * N;
int a[N], son[M][2], idx;
int n;
void insert(int x){
int p = 0;
for(int i = 30; i >= 0; i--){
int u = x >> i & 1;// 第i+1位上的数字
if(!son[p][u]) son[p][u] = ++idx;
p = son[p][u];
}
}
int query(int x){
int p = 0, res = 0;// res记录和当前x异或后结果的最大值
for(int i = 30; i >= 0; i--){
int u = x >> i & 1;
if(son[p][!u]){// 从最高位开始,每次尽量走与当前位相反的路
p = son[p][!u];
res = res*2 + 1;
}else{
p = son[p][u];
res = res*2;
}
}
return res;
}
int main(){
scanf("%d", &n);
for(int i = 0; i < n; i++){
scanf("%d", &a[i]);
insert(a[i]);
}
int res = 0;
for(int i = 0; i < n; i ++) res = max(res, query(a[i]));
printf("%d", res);
return 0;
}
(5)并查集
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int p[N];
int n, m;
int find(int x){
if(p[x] != x) p[x] = find(p[x]);// 路径压缩
return p[x];
}
int main(){
scanf("%d%d", &n, &m);
for(int i = 0; i < n; i++) p[i] = i;
while(m--){
char op[2];
int a, b;
scanf("%s%d%d", op, &a, &b);
if(*op == 'M') p[find(a)] = find(b);
else{
if(find(a) == find(b)) puts("Yes");
else puts("No");
}
}
return 0;
}
// AcWing 837. 连通块中点的数量
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int p[N], cnt[N];
int n, m;
int find(int x){
if(p[x] != x) p[x] = find(p[x]);// 路径压缩
return p[x];
}
int main(){
scanf("%d%d", &n, &m);
for(int i = 0; i < n; i++) p[i] = i, cnt[i] = 1;
while(m--){
char op[5];
int a, b;
scanf("%s", op);
if(op[0] == 'C'){
scanf("%d%d", &a, &b);
a = find(a), b = find(b);
if(a == b) continue;// 注意a和b可能已经在同一个集合里
p[a] = b;
cnt[b] += cnt[a];
}else if(op[1] == '1'){
scanf("%d%d", &a, &b);
if(find(a) == find(b)) puts("Yes");
else puts("No");
}else{
scanf("%d", &a);
printf("%d\n", cnt[find(a)]);
}
}
return 0;
}
// Acwing 240. 食物链
#include <iostream>
using namespace std;
const int N = 5e5 + 10;
int n, k;
int p[N], d[N];
// d[i]表示节点i到根结点的距离, 用距离来表示节点和根结点之间的关系
// 其中距离为1表示可以吃根结点;距离为2表示可以被根结点吃;距离为3表示和根结点是同类
int find(int x){
if(p[x] != x){
int t = find(p[x]);// 确保先调用find(p[x]),更新d[p[x]]
d[x] += d[p[x]];
p[x] = t;
}
return p[x];
}
int main(){
scanf("%d%d", &n, &k);
for(int i = 0; i < n; i++) p[i] = i;
int res = 0;
while(k--){
int t, x, y;
scanf("%d%d%d", &t, &x, &y);
if(x > n || y > n) res++;
else{
int px = find(x), py = find(y);
if(t == 1){// 判断是否为同类
// 如果x和y在同一个集合里 并且 x、y到根结点的距离差mod3不等于0,说明x和y不是同类
if(px == py && (d[x] - d[y]) % 3) res ++;
// 如果x和y不在一个集合里,合并到一个集合里
else if(px != py){
p[px] = py;
// 令x和y为同类,则(d[x]+?-d[y])mod3=0,则?=d[y]-d[x]
d[px] = d[y]-d[x];
}
}else{// 判断x是否吃y
// 如果x和y在同一个集合里 并且 x、y到根结点的距离差mod3不等于1,说明x不吃y
if(px == py && (d[x] - d[y] - 1) % 3) res ++;
// 如果x和y不在一个集合里,合并到一个集合里并使x吃y
else if(px != py){
p[px] = py;
// 令x吃y,则(d[x]+?-d[y]-1)mod3=0,则?=d[y]+1-d[x]
d[px] = d[y]+1-d[x];
}
}
}
}
printf("%d", res);
return 0;
}
(6)堆
(7)Hash表
开放寻址法
#include <iostream>
#include <cstring>
using namespace std;
const int N = 200003, INF = 0x3f3f3f3f;
// 开放寻址法中,hash表表长一般为表中元素个数的2~3倍的质数
int h[N];
int n;
// 要么返回x的位置,要么返回k后第一个空的位置。
int find(int x){
int k = (x % N + N) % N;
while(h[k] != INF && h[k] != x){
k++;
if(k == N) k = 0;// 循环找
}
return k;
}
int main(){
scanf("%d", &n);
memset(h, 0x3f, sizeof(h));// 按字节赋值
while(n--){
char op[2];
int x;
scanf("%s%d", op, &x);
if(*op == 'I') h[find(x)] = x;
else{
if(h[find(x)] == INF) puts("No");
else puts("Yes");
}
}
return 0;
}
0x3f3f3f3f
在算法竞赛中,我们常常需要用到设置一个常量用来代表“无穷大”。
比如对于int类型的数,有的人会采用INT_MAX,即0x7fffffff作为无穷大。但是以INT_MAX为无穷大常常面临一个问题,即加一个其他的数会溢出。
而这种情况在动态规划,或者其他一些递推的算法中常常出现,很有可能导致算法出问题。
所以在算法竞赛中,我们常采用0x3f3f3f3f来作为无穷大。0x3f3f3f3f主要有如下好处:
0x3f3f3f3f的十进制为1061109567,和INT_MAX一个数量级,即109数量级,而一般场合下的数据都是小于109的。
0x3f3f3f3f * 2 = 2122219134,无穷大相加依然不会溢出。
拉链法
#include <iostream>
#include <cstring>
using namespace std;
const int N = 100003;// hash表表长最好为质数
int h[N], e[N], ne[N], idx;
int n;
void insert(int x){
int k = (x % N + N) % N;// 散列化,+N防止负数
/*
一般余数应为正数。
在c++中,-10%3=-1;正确计算应该10=-4*3+2,-10%3=2。
不能(x+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];
int x;
scanf("%s%d", op, &x);
if(*op == 'I') insert(x);
else{
if(find(x)) puts("Yes");
else puts("No");
}
}
return 0;
}
字符串哈希
// AcWing 841. 字符串哈希
#include <iostream>
using namespace std;
typedef unsigned long long ULL;// unsigned用来mod Q(=2^64)
const int N = 1e5 + 10, P = 131;// P为P进制,一般取131或者13331,不容易冲突
int n, m;
char str[N];
ULL h[N], p[N];// 哈希值前缀和;p[i]表示p^i
ULL get(int l, int r){
return h[r] - h[l-1] * p[r-l+1]; // 前缀和
/*
h[r]为1到r位字符串的哈希值
h[l-1]为1到l-1位字符串的哈希值
* p[r-l+1]为了让 1到l-1位 左边对齐到 1到r位
*/
}// 返回[l, r]区间内字符串的哈希值
int main(){
scanf("%d%d", &n, &m);
scanf("%s", str + 1);
// 打表
p[0] = 1;
for(int i = 1; i <= n; i++){
h[i] = h[i-1] * P + str[i];// str[i]自动转化为ascii码,128个
p[i] = p[i-1] * P;
}
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
时间复杂度O(n+m),空间复杂度O(n)
基本实现
递归
// AcWing 842. 排列数字
#include <iostream>
using namespace std;
const int N = 10;
int n;
int path[N];// path[i]表示第i位上的数字,从0开始
bool st[N];// st[i]表示i是否被用过
void dfs(int u){// u表示当前走到第i位
// 递归结束的条件
if(u == n){
for(int i = 0; i < n; i++) printf("%d ", path[i]);
puts("");
return;
}
// 递归
for(int i = 1; i <= n; i++)
if(!st[i]){// 如果i还没有被使用过
path[u] = i;
st[i] = true;// 状态标记
dfs(u+1);
st[i] = false;// 递归后恢复
}
}
int main(){
scanf("%d", &n);
dfs(0);
return 0;
}
// AcWing 846. 树的重心
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e5 + 10, M = N * 2;
int n;
int h[N], e[M], ne[M], idx;
// 用邻接表表示(以边为主体节点)
// e[i],i是边编号,值是i这条边终点的结点编号
int ans = N;// 最终结果
bool st[N];// dfs标记,st[i]表示编号为i的节点是否遍历过
// 添加从a到b的单向边
void add(int a, int b){
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
// 求以u为根的子树中包含的节点数量
int dfs(int u){
st[u] = true;
int sum = 1, res = 0;// 整棵树的节点数;删除该节点后子树中各连通块节点数最大值
for(int i = h[u]; i != -1; i = ne[i]){// 遍历该节点的邻接表
int j = e[i];
if(!st[j]){// 如果j这个点没有搜过
int s = dfs(j);
res = max(res, s);
sum += s;
}
}
res = max(res, n - sum);// 比较节点的子树中各连通块节点数、除了以u节点为根的树以外的节点总数
ans = min(ans, res);// 比较最小值
return sum;
}
int main(){
cin >> n;
memset(h, -1, sizeof h);
// 输入n-1条边
for(int i = 0; i < n-1; i ++){
int a, b;
cin >> a >> b;
add(a, b), add(b, a);
}
dfs(1);
cout << ans << endl;
return 0;
}
栈
vector<vector<int>> adj; // 邻接表
vector<bool> vis; // 记录节点是否已经遍历
void dfs(int s) {
stack<int> st;
st.push(s);
vis[s] = true;
while (!st.empty()) {
int u = st.top();
st.pop();
for (int v : adj[u]) {
if (!vis[v]) {
vis[v] = true; // 确保栈里没有重复元素
st.push(v);
}
}
}
}
连通性模型
状态是棋子不回溯,状态是棋盘要回溯。
// AcWing 1113. 红与黑
#include <iostream>
#include <cstring>
using namespace std;
const int N = 30;
char g[N][N];
int w, h, sx, sy;
bool st[N][N];
int dfs(int sx, int sy){
int res = 1;
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
st[sx][sy] = true;
for(int i = 0; i < 4; i++){
int a = sx + dx[i], b = sy + dy[i];
if(a < 0 || a >= h || b < 0 || b >= w) continue;
if(g[a][b] != '.') continue;
if(st[a][b]) continue;
res += dfs(a, b);
}
return res;
}
int main(){
while(cin >> w >> h, w || h){
for(int i = 0; i < h; i++)
for(int j = 0; j < w; j++){
cin >> g[i][j];
if(g[i][j] == '@') sx = i, sy = j;
}
memset(st, 0, sizeof st);
cout << dfs(sx, sy) << endl;
}
return 0;
}
搜索顺序
// AcWing 1116. 马走日
#include <iostream>
#include <cstring>
using namespace std;
const int N = 10;
int n, m, x, y;
bool st[N][N];
int res; // 能遍历棋盘的途径总数
void dfs(int sx, int sy, int cnt){ // cnt为当前在找第几个点
if(cnt == n * m){
res ++;
return;
}
st[sx][sy] = true;
int dx[8] = {-1, -2, -2, -1, 1, 2, 2, 1}, dy[8] = {-2, -1, 1, 2, 2, 1, -1, -2};
for(int i = 0; i < 8; i++){
int a = sx + dx[i], b = sy + dy[i];
if(a < 0 || a >= n || b < 0 || b >= m) continue;
if(st[a][b]) continue;
dfs(a, b, cnt+1);
}
st[sx][sy] = false; // 状态是棋盘需要恢复现场
}
int main(){
int t; cin >> t;
while(t--){
cin >> n >> m >> x >> y;
res = 0;
dfs(x, y, 1);
cout << res << endl;
}
return 0;
}
// AcWing 1118. 分成互质组
#include <iostream>
#include <cstring>
using namespace std;
const int N = 10;
int n;
int nums[N], g[N][N]; // g是组
bool st[N];
int ans = N;
int gcd(int a, int b){ // 求最大公约数
return b ? gcd(b, a%b) : a;
}
bool check(int g[], int gc, int i){ // check i能否插入组内
for(int j = 0; j < gc; j++)
if(gcd(nums[g[j]], nums[i]) > 1) return false;
return true;
}
void dfs(int u, int gc, int tc, int start){ // 当前搜第几个组,当前组里有几个元素,当前一共搜了几个元素,当前组从哪个下标开始搜
if(u >= ans) return;
if(tc == n) ans = u;
bool flag = true; // 当前组是否搜完,如果下面的for循环结束了都没找到一个元素说明当前组已经搜完
for(int i = start; i < n; i++)
if(!st[i] && check(g[u], gc, i)){
g[u][gc] = i;
st[i] = true;
dfs(u, gc+1, tc+1, i+1);
st[i] = false;
flag = false;
}
if(flag) dfs(u+1, 0, tc, 0);
}
int main(){
cin >> n;
for(int i = 0; i < n; i ++) cin >> nums[i];
dfs(1, 0, 0, 0);
cout << ans << endl;
return 0;
}
剪枝与优化
// AcWing 165. 小猫爬山
// 按分成互斥组的写法会TLE
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 20;
int n, w;
int c[N];
int sum[N]; // 每个组的重量
int ans = N;
void dfs(int u, int k){ // 当前安排第u只猫, 当前共有几个车可以安排
// 最优性剪枝
if(k >= ans) return;
if(u == n){
ans = k;
return;
}
for(int i = 0; i < k; i++){ // 分支1:塞到某辆车
if(sum[i] + c[u] <= w){ // 可行性剪枝
sum[i] += c[u];
dfs(u + 1, k);
sum[i] -= c[u]; // 恢复现场
}
}
// 分支2:新开一辆车
sum[k] = c[u];
dfs(u+1, k+1);
sum[k] = 0; // 恢复现场
}
int main(){
cin >> n >> w;
for(int i = 0; i < n; i ++) cin >> c[i];
sort(c, c + n); // 优化搜索顺序:优先搜索分支较少的节点(胖猫)
reverse(c, c + n);
dfs(0, 0);
cout << ans << endl;
return 0;
}
// AcWing 166. 数独
// 神代码
#include <iostream>
using namespace std;
const int N = 9, M = 1 << N;
int ones[M], map[M];
int row[N], col[N], cell[3][3]; // row[i]表示第i行的二进制表示(1表示可用,0表示不可用)
char str[100];
int lowbit(int x){
return x & -x;
}
void init(){
for(int i = 0; i < N; i++)
row[i] = col[i] = (1 << N) - 1;
for(int i = 0; i < 3; i++)
for(int j = 0; j < 3; j++)
cell[i][j] = (1 << N) - 1;
}
void draw(int x, int y, int t, bool is_set){ // 在(x,y)填上or删掉t
if(is_set) str[x*N+y] = '1' + t;
else str[x*N+y] = '.';
int v = 1 << t;
if(!is_set) v = -v;
row[x] -= v;
col[y] -= v;
cell[x/3][y/3] -= v;
}
int get(int x, int y){ // 位运算优化,返回哪个数字能用
return row[x] & col[y] & cell[x/3][y/3];
}
bool dfs(int cnt){
if(!cnt) return true;
int minv = 10;
int x, y;
for(int i = 0; i < N; i++)
for(int j = 0; j < N; j++) // 优化搜索顺序:找到某个位置可填数字最少的
if(str[i*N+j] == '.'){
int state = get(i, j);
if(ones[state] < minv){
minv = ones[state];
x = i, y = j;
}
}
int state = get(x, y); // 尝试填数字
for(int i = state; i; i -= lowbit(i)){
int t = map[lowbit(i)];
draw(x, y, t, true);
if(dfs(cnt-1)) return true;
draw(x, y, t, false);
}
return false;
}
int main(){
for(int i = 0; i < N; i++) map[1 << i] = i; // 打表map[2^i] = i
for(int i = 0; i < 1 << N; i++)
for(int j = 0; j < N; j++) // 打表one[i] = i二进制表示里有几个1
ones[i] += i >> j & 1;
while(cin >> str, str[0] != 'e'){
init();
int cnt = 0; // 有几个位置已经填上数字
for(int i = 0, k = 0; i < N; i++)
for(int j = 0; j < N; j++, k++)
if(str[k] != '.'){
int t = str[k] - '1';
draw(i, j, t, true);
}else cnt ++;
dfs(cnt);
puts(str);
}
return 0;
}
// AcWing 167. 木棒
// 好多细节
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 70;
int n;
int w[N], sum, length;
bool st[N];
bool dfs(int u, int cur, int start){ // 当前已经搜索几根木棒,当前组棒的长度,从哪根棍开始搜
if(u * length == sum) return true;
if(cur == length) return dfs(u+1, 0, 0);
for(int i = start; i < n; i++){
if(st[i] || cur + w[i] > length) continue;
st[i] = true;
if(dfs(u, cur+w[i], i+1)) return true;
st[i] = false;
if(!cur || cur + w[i] == length) return false; // 等效冗余:第一根棍or最后一根棍失败,剪枝。
int j = i; // 等长木棍略过
while(j < n && w[j] == w[i]) j++;
i = j-1; // =j-1是因为for循环还要让i+1
}
return false;
}
int main(){
while(cin >> n, n){
memset(st, 0, sizeof st);
sum = 0;
for(int i = 0; i < n; i ++){
cin >> w[i];
sum += w[i];
}
sort(w, w+n); // 优化搜索顺序,优先搜索长木棍(木棒>木棍)
reverse(w, w+n);
length = 1;
while(true){
if(sum % length == 0 && dfs(0, 0, 0)){ // 枚举木棒原始长度,木棒长度应为木棍总长度的约数
cout << length << endl;
break;
}
length ++;
}
}
return 0;
}
迭代加深
双向DFS
IDA*
(2)BFS
基本BFS
// AcWing 844. 走迷宫
// 求距离
#include <iostream>
#include <queue>
#include <cstring>
using namespace std;
typedef pair<int, int> PII; // 坐标
const int N = 110;
int n, m;
int g[N][N], d[N][N];
// g迷宫;
// d标记状态,d[x][y]表示(x, y)到原点的距离
int bfs(){
// bfs队列
queue<PII> q;
q.push({0, 0});
// 初始化数组
memset(d, -1, sizeof d);
d[0][0] = 0;
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
// (-1, 0)(0, 1)(1, 0)(0, -1)表示⬅️⬆️➡️⬇️(dx和dy写反了
while(q.size()){
// 弹出队头
auto t = q.front();
q.pop();
// 遍历邻近节点
for(int i = 0; i < 4; i++){
int x = t.first + dx[i], 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(){
scanf("%d%d", &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;
}
// AcWing 847. 图中点的层次
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 1e5+10;
int n, m;
int h[N], e[N], ne[N], idx;
int d[N];
void add(int a, int b){
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
int bfs(){// 返回最短路径
queue<int> q;
q.push(1);
memset(d, -1, sizeof d);
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;
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;
}
Flood Fill
// AcWing 1097. 池塘计数
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
#define x first
#define y second
typedef pair<int, int> PII;
const int N = 1010, M = N * N;
int n, m;
char g[N][N];
bool st[N][N];
PII q[M]; // 数组模拟队列
void bfs(int sx, int sy){ // 从起点(sx, sy)八连通flood_fill
int hh = 0, tt = 0;
q[0] = {sx, sy};
st[sx][sy] = true;
while(hh <= tt){
PII t = q[hh++]; // 取下队头
for(int i = t.x - 1; i <= t.x + 1; i ++)
for(int j = t.y - 1; j <= t.y + 1; j++){ // 八连通
if(i == t.x && j == t.y) continue; // 起点
if(i < 0 || i >= n || j < 0 || j >= m) continue; // 越界
if(g[i][j] == '.' || st[i][j]) continue; // 不含雨水||已经遍历过
q[++tt] = {i, j}; // 入队
st[i][j] = true;
}
}
}
int main(){
scanf("%d%d", &n, &m);
for(int i = 0; i < n; i++) scanf("%s", g[i]); // 按行读入
int cnt = 0;
for(int i = 0; i < n; i++)
for(int j = 0; j < m; j++)
if(g[i][j] == 'W' && !st[i][j]){ // 含雨水&&没有遍历过,则开始flood_fill
bfs(i, j);
cnt ++;
}
printf("%d\n", cnt);
return 0;
}
最短路模型
从一个点走到另一个点
// AcWing 1076. 迷宫问题
// 求最短路径
#include <iostream>
#include <cstring>
using namespace std;
#define x first
#define y second
typedef pair<int, int> PII;
const int N = 1010, M = N*N;
int n;
int g[N][N];
PII q[M];
PII pre[N][N]; // pre[x][y] = t表示点(x,y)是从点t来的(存路径同时记录st)
void bfs(int sx, int sy){
int dx[4] = {0, -1, 0, 1}, dy[4] = {-1, 0, 1, 0};
int hh = 0, tt = 0;
q[0] = {sx, sy}; // 找到的第一条路径就是最短路径,不用记录st
memset(pre, -1, sizeof pre);
pre[sx][sy] = {0, 0};
while(hh <= tt){
PII t = q[hh++];
for(int i = 0; i < 4; i++){
int a = t.x + dx[i], b = t.y + dy[i];
if(a < 0 || a >= n || b < 0 || b >= n) continue;
if(g[a][b]) continue;
if(pre[a][b].x != -1) continue; // 走过了
q[++tt] = {a, b};
pre[a][b] = t;
}
}
}
int main(){
scanf("%d", &n);
for(int i = 0; i < n; i++)
for(int j = 0; j < n; j++)
scanf("%d", &g[i][j]);
bfs(n-1, n-1); // 倒着搜
PII end(0, 0);
while(true){
printf("%d %d\n", end.x, end.y);
if(end.x == n-1 && end.y == n-1) break;
end = pre[end.x][end.y];
}
return 0;
}
最小步数模型
矩阵从一个状态变成另一种状态
// AcWing 1107. 魔板
#include <iostream>
#include <unordered_map>
#include <queue>
#include <algorithm>
using namespace std;
char g[2][4];
unordered_map<string, pair<char, string>> pre; // 状态,(操作,pre状态)
unordered_map<string, int> dist; // 状态,距离
void set(string state){
for(int i = 0; i < 4; i++) g[0][i] = state[i];
for(int i = 7, j = 0; j < 4; i--, j++) g[1][j] = state[i];
}
string get(){
string res;
for(int i = 0; i < 4; i++) res += g[0][i];
for(int i = 3; i >= 0; i--) res += g[1][i];
return res;
}
string move0(string state){
set(state);
for(int i = 0; i < 4; i++) swap(g[0][i], g[1][i]);
return get();
}
string move1(string state){
set(state);
int v0 = g[0][3], v1 = g[1][3];
for(int i = 3; i > 0; i--){
g[0][i] = g[0][i-1];
g[1][i] = g[1][i-1];
}
g[0][0] = v0, g[1][0] = v1;
return get();
}
string move2(string state){
set(state);
int v = g[0][1];
g[0][1] = g[1][1];
g[1][1] = g[1][2];
g[1][2] = g[0][2];
g[0][2] = v;
return get();
}
int bfs(string start, string end){
if(start == end) return 0;
queue<string> q;
q.push(start);
dist[start] = 0;
while(!q.empty()){
auto t = q.front();
q.pop();
string m[3];
m[0] = move0(t), m[1] = move1(t), m[2] = move2(t);
for(int i = 0; i < 3; i++){
if(!dist.count(m[i])){
dist[m[i]] = dist[t] + 1;
pre[m[i]] = {'A'+ i, t};
q.push(m[i]);
if(m[i] == end) return dist[end];
}
}
}
return -1;
}
int main(){
string start, end;
for(int i = 0, x; i < 8; i++){
cin >> x;
end += char(x + '0'); // 数字转字符
}
for(int i = 1; i <= 8; i++) start += char(i + '0');
int step = bfs(start, end);
cout << step << endl;
string res;
while(end != start){
res += pre[end].first;
end = pre[end].second;
}
reverse(res.begin(), res.end());
if(step > 0) cout << res << endl;
return 0;
}
双端队列BFS
// AcWing 175. 电路维修
// 该图:点之间能走通可以看作边权为0,不相连看作边权为1;横纵坐标之和为奇数的点永远无法到达。
#include <iostream>
#include <deque>
#include <algorithm>
#include <cstring>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 510;
int r, c;
char g[N][N];
int dist[N][N];
bool st[N][N];
int bfs(){
deque<PII> q;
q.push_back({0, 0});
memset(dist, 0x3f, sizeof dist);
dist[0][0] = 0;
memset(st, 0, sizeof st);
int dx[4] = {-1, -1, 1, 1}, dy[4] = {-1, 1, 1, -1}; // 通过当前格点计算四个方向的格点坐标(逆时针)
int ix[4] = {-1, -1, 0, 0}, iy[4] = {-1, 0, 0, -1}; // 通过当前格点计算边坐标(g[x][y],逆时针)
char ch[] = "\\/\\/"; // \/\/,四个方向的边,逆时针
while(q.size()){
auto t = q.front();
q.pop_front();
int x = t.x, y = t.y;
if(st[x][y]) continue;// (点第一次出队列时才是最优的,入队列不一定)
st[x][y] = true;
for(int i = 0; i < 4; i++){
int a = x + dx[i], b = y + dy[i]; // 四个方向的格点坐标
if(a < 0 || a > r || b < 0 || b > c) continue;
int ga = x + ix[i], gb = y + iy[i]; // 四个方向边坐标
int w = (g[ga][gb] != ch[i]);
int d = dist[x][y] + w;
if(d < dist[a][b]){ // 更新四个方向格点的距离
dist[a][b] = d;
if(!w) q.push_front({a, b}); // 边权为0(能走通),插入队头
else q.push_back({a, b}); // 边权为1(不能走通),插入队尾
}
}
}
return dist[r][c];
}
int main(){
int t; scanf("%d", &t);
while(t--){
scanf("%d%d", &r, &c);
for(int i = 0; i < r; i++) scanf("%s", g[i]);
int res = bfs();
if(res == 0x3f3f3f3f) puts("NO SOLUTION"); // 性质:下标之和为奇数的格点无法到达
else printf("%d\n", res);
}
return 0;
}
// 汗流浃背了,好难
// P1948 [USACO08JAN] Telephone Lines S
优先队列BFS
双向BFS
// AcWing 190. 字串变换
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
#include <unordered_map>
using namespace std;
const int N = 6;
int n;
string A, B;
string a[N], b[N];
int extend(queue<string>& q, unordered_map<string, int>& da, unordered_map<string, int>& db, string a[N], string b[N]){
// 扩展a
int d = da[q.front()];
while(q.size() && da[q.front()]==d){ // 和队头元素在同一层(每次扩展一层,而不是一个)
auto t = q.front();
q.pop();
for(int i = 0; i < n; i++) // 遍历所有的替换规则
for(int j = 0; j < t.size(); j++)
if(t.substr(j, a[i].size()) == a[i]){ // 如果t串中包含字符串a
string r = t.substr(0, j) + b[i] + t.substr(j + a[i].size()); // 替换
if(db.count(r)) return da[t] + db[r] + 1;
if(da.count(r)) continue;
da[r] = da[t] + 1;
q.push(r);
}
}
return 11;
}
int bfs(){
if(A == B) return 0;
queue<string> qa, qb;
unordered_map<string, int> da, db; // dist
qa.push(A), qb.push(B);
da[A] =0, da[B] = 0;
int step = 0;
while(qa.size() && qb.size()){
// 每次选择节点少的队列扩展,总共搜到的点数会更少
int t = qa.size() < qb.size() ? extend(qa, da, db, a, b) : extend(qb, db, da, b, a);
if(t <= 10) return t;
if(++step == 10) return -1;
}
return -1;
}
int main(){
cin >> A >> B;
while(cin >> a[n] >> b[n]) n++;
int res = bfs();
if(res == -1) puts("NO ANSWER!");
else cout << res << endl;
return 0;
}
A*
一定有解的情况下才能使用
// AcWing 179. 八数码
// 八数码问题:如果读出来的逆序对的数量是偶数,则一定有解!
#include <iostream>
#include <algorithm>
#include <queue>
#include <unordered_map>
using namespace std;
typedef pair<int, string> PIS;
int f(string state){// 估价函数,应使终点估计距离f(state) <= 终点真实距离
int res = 0;
for(int i = 0; i < state.size(); i++){
if(state[i] != 'x'){
int t = state[i] - '1';
res += abs(i/3 - t/3) + abs(i%3 - t%3); // 当前坐标(i/3, i%3) 真实坐标(t/3, t%3)
}
}
return res; // 曼哈顿距离之和
}
string bfs(string start){
string end = "12345678x";
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
char op[4] = {'u', 'r', 'd', 'l'};
unordered_map<string, int> dist; // 状态,距离
unordered_map<string, pair<string, char>> prev; // 状态,(前一个状态,操作)
priority_queue<PIS, vector<PIS>, greater<PIS>> heap; // 小根堆(起点距离+终点估计距离,状态)
dist[start] = 0;
heap.push({f(start), start});
while(heap.size()){
auto t = heap.top(); // 每次取出起点距离+估计距离最小的
heap.pop();
string state = t.second;
if(state == end) break; // 只保证终点第一次出队时的最短路径是最优解
int x, y;
for(int i = 0; i < state.size(); i++) // 求x的坐标
if(state[i] == 'x'){
x = i / 3, y = i % 3;
break; }
int step = dist[state];
string source = state; // 记录source
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(state[x*3+y], state[a*3+b]);
if(!dist.count(state) || dist[state] > step + 1){ // 更新距离
dist[state] = step + 1;
prev[state] = {source, op[i]};
heap.push({dist[state] + f(state), state});
}
swap(state[x*3+y], state[a*3+b]);
}
}
}
string res;
while(end != start){
res += prev[end].second;
end = prev[end].first;
}
reverse(res.begin(), res.end());
return res;
}
int main(){
string start, seq;
char c;
while(cin >> c){
start += c;
if(c != 'x') seq += c;
}
int cnt = 0; // 逆序对数量
for(int i = 0; i < seq.size(); i++)
for(int j = i + 1; j < seq.size(); j++)
if(seq[i] > seq[j]) cnt++;
if(cnt & 1) puts("unsolvable");
else cout << bfs(start) << endl;
return 0;
}
// AcWing 178. 第K短路
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
typedef pair<int, int> PII;
typedef pair<int, PII> PIII;
const int N = 1010, M = 20010;
int n, m, s, t, k;
int h[N], rh[N], e[M], w[M], ne[M], idx;
int dist[N], cnt[N];
bool st[N];
void add(int h[], int a, int b, int c){
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}
void dijkstra(){ // 反向dijkstra(对反向图跑dijkstra,获得终点t到任意点的最短路径长度,作为A*的估计距离)
priority_queue<PII, vector<PII>, greater<PII>> heap; // (dist, 下标)
heap.push({0, t});
memset(dist, 0x3f, sizeof dist);
dist[t] = 0;
while(heap.size()){
auto tmp = heap.top();
heap.pop();
int ver = tmp.second;
if(st[ver]) continue;
st[ver] = true;
for(int i = rh[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});
}
}
}
}
int astar(){
priority_queue<PIII, vector<PIII>, greater<PIII>> heap; // (估计距离, (已走距离, 下标))
heap.push({dist[s], {0, s}});
while(heap.size()){
auto tmp = heap.top();
heap.pop();
int ver = tmp.second.second, distance = tmp.second.first;
cnt[ver] ++; // 某个点第k次弹出时,就是第k短路径
if(cnt[t] == k) return distance;
for(int i = h[ver]; i != -1; i = ne[i]){
int j = e[i];
if(cnt[j] < k) heap.push({distance + w[i] + dist[j], {distance + w[i], j}}); // 限定每个点的访问次数
}
}
return -1;
}
int main(){
memset(h, -1, sizeof h);
memset(rh, -1, sizeof rh);
cin >> n >> m;
for(int i = 0, a, b, c; i < m; i++){
cin >> a >> b >> c;
add(h, a, b, c), add(rh, b, a, c);
}
cin >> s >> t >> k;
if(s == t) k++; // 起点=终点时,存在一条长度为0的自环
dijkstra();
cout << astar() << endl;
return 0;
}
估价函数
- 只允许上下左右四个方向移动=>曼哈顿距离
- 允许朝八个方向移动=>对角距离
- 允许朝任意方向移动=>欧几里得距离
// 曼哈顿距离
dx + dy
// 对角距离(?
D * (dx + dy) + (D2 - 2 * D) * min(dx, dy)
// 欧几里得距离
sqrt(dx * dx + dy * dy)
多源BFS
// AcWing 173. 矩阵距离
// 求01矩阵每个0到1的最短距离(曼哈顿距离=行距离+列距离,单源BFS情况下会超时)
#include <iostream>
#include <cstring>
using namespace std;
#define x first
#define y second
typedef pair<int, int> PII;
const int N = 1010, M = N * N;
int n, m;
char g[N][N];
PII q[M];
int dist[N][N];
void bfs(){
memset(dist, -1, sizeof dist);
int hh = 0, tt = -1;
for(int i = 0; i < n; i++) // 找起点
for(int j = 0; j < m; j++)
if(g[i][j] == '1'){
dist[i][j] = 0; // 起点到虚拟源点的距离为0
q[++tt] = {i, j};
}
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
while(hh <= tt){
PII t = q[hh++];
for(int i = 0; i < 4; i++){
int a = t.x + dx[i], b = t.y + dy[i];
if(a < 0 || a >= n || b < 0 || b >= m) continue;
if(dist[a][b] != -1) continue;
dist[a][b] = dist[t.x][t.y] + 1;
q[++tt] = {a, b};
}
}
}
int main(){
scanf("%d%d", &n, &m);
for(int i = 0; i < n; i++) scanf("%s", g[i]);
bfs();
for(int i = 0; i < n; i++){
for(int j = 0; j < m; j++) printf("%d ", dist[i][j]);
puts("");
}
return 0;
}
(3)最短路径
Dijkstra
朴素的Dijkstra
- 稠密图用邻接矩阵存储
// AcWing 849. Dijkstra求最短路 I
#include <iostream>
#include <cstring>
using namespace std;
const int N = 510, INF = 0x3f3f3f3f;
int n, m;
// 以下下标都从1开始
int g[N][N];// 图的邻接矩阵
int dist[N];// 各点到1号的最短距离
bool st[N];// 某点是否已确定最短路径
int dijkstra(){
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
// 需要进行n-1次的求最短路径
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; // 未确定最短路径 并且 到源点的距离比dist[t]还小
// 用新点更新最短距离
for(int j = 1; j <= n; j++)
dist[j] = min(dist[j], dist[t] + g[t][j]);
// 更新标记
st[t] = true;
}
return dist[n] == INF ? -1 : 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
- 稀疏图用邻接表存储
// AcWing 850. Dijkstra求最短路 II
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
typedef pair<int, int> PII;// 距离,点
const int N = 1e6 + 10, INF = 0x3f3f3f3f;
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 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;
// 如果已经处理过该点,跳过。
if(st[ver]) continue;
// 如果没有,处理
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});
}
}
st[ver] = true;// 更新标记
}
return dist[n] == INF ? -1 : dist[n];
}
int main(){
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h); // 初始化邻接表
while(m--){
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);// 添加重边也没关系,有堆做优化
}
printf("%d", dijkstra());
return 0;
}
Bellman-Ford
/*
有向图中存在负权边,则不一定存在最短路径。如果存在最短路径,则最短路径上肯定没有负环。
假设存在n个点,如果点1到n的最短路径存在,则最短路径的长度等于n-1。
如果存在负环,则第n次时还能进行松弛操作。
*/
for(int i = 0; i < n; i++)
/*
有向图中,如果所有边都满足三角形不等式dist[y]<=dist[x]+c,则dist数组就是所求最短路。
*/
for(int j = 0; j < m; j++){
auto e = edges[j];
dist[e.b] = min(dist[e.b], dist[e.a] + e.c);// 松弛操作
}
// AcWing 853. 有边数限制的最短路
#include <iostream>
#include <cstring>
using namespace std;
const int N = 510, M = 10010;
int n, m, k;
int dist[N], last[N];
struct Edge{
int a, b, c;
}edges[M];
void bellman_ford(){
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
// 走k步
for(int i = 0; i < k; i++){
memcpy(last, dist, sizeof dist);// 拷贝上一次迭代的结果,避免k步限制失效,发生串联
// 遍历所有的边
for(int j = 0; j < m; j++){
auto e = edges[j];
dist[e.b] = min(dist[e.b], last[e.a] + e.c);
}
}
}
int main(){
cin >> n >> m >> k;
for(int i = 0; i < m; i++){
int a, b, c;
cin >> a >> b >> c;
edges[i] = {a, b, c};
}
bellman_ford();
/*
用大于INF/2判断是否存在最短路径,可能存在未更新的点同样被边更新的情况。
到不了的几个点之间存在负权边。使得到不了的点的距离一定范围内减小,小于了INF。这时可以使用小一点的INF/2来做判断,大于INF/2则认为不存在路径。
*/
if(dist[n] > 0x3f3f3f3f/2) puts("impossible");
else cout << dist[n];
return 0;
}
SPFA
Shortest Path Fast Algorithm
队列优化的Bellman-Ford算法
- 图中没有负环才能用SPFA
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 1e5+10;
int n, m;
int h[N], e[N], w[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();
st[t] = false;// 解除标记,曾经加入过队列的点出队后,会再次被加入队列,再次加入后继续更新与它联通的点
// 扫描该节点的所有出边
for(int i = h[t]; i != -1; i = ne[i]){
int j = e[i];
/*
bellman-ford是不管这条边是否满足不等式,都要进行处理。
spfa通过队列、该点的出边、不满足不等式,进行扩展处理。避免对不需要扩展的节点的冗余扫描。
*/
// 如果不满足不等式
if(dist[j] > dist[t] + w[i]){
dist[j] = dist[t] + w[i];
// 如果没有处理过
if(!st[j]){
q.push(j);
st[j] = true;
}
}
}
}
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();
/*
spfa是基于拓扑序列的,不存在bellman-ford算法中未更新的点同样被边更新的情况。所以用==0x3f3f3f3f来判断是否存在最短路径。
*/
if(t == 0x3f3f3f3f) puts("impossible");
else cout << t;
return 0;
}
// AcWing 852. spfa判断负环
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 2010, M = 10010;
int n, m;
int h[N], w[M], e[M], ne[M], idx;
int dist[N], cnt[N];// cnt记录到1到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++;
}
bool spfa(){
// 找负环无需初始化dist数组,因为dist数组一开始已经是0了,所以接下来只会更新小于0的边权,也就是负权边,存在负环的话就会不停的更新
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;
// 最短路径超过n条边
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()) puts("Yes");
else puts("No");
return 0;
}
Floyd
#include <iostream>
using namespace std;
const int N = 210, INF = 1e9;
int n, m, k;
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]);
/*
d[k, i, j]表示从i出发,只经过1~k点,到达j的最短路径。
dp。确定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, c;
cin >> a >> b >> c;
d[a][b] = min(d[a][b], c); // 处理重边
}
floyd();
while(k--){
int a, b;
cin >> a >> b;
int t = d[a][b];
if(t > INF/2) puts("impossible");
else cout << t << endl;
}
return 0;
}
(4)最小生成树
Prim
朴素的Prim
#include <iostream>
#include <cstring>
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++){ // 遍历n次,找n个点加入集合
int t = -1;
for(int j = 1; j <= n; j++) // 找到集合外距离集合最近的点
if(!st[j] && (t == -1 || dist[j] < dist[t]))
t = j;
if(i && dist[t] == INF) return INF; // 不是第一个点 且 t到集合的距离是INF(说明当前集合是不连通的)
if(i) res += dist[t]; // 不是第一个点,加入集合
st[t] = true;
for(int j = 1; j <= n; j++) dist[j] = min(dist[j], g[t][j]); // 更新所有点到集合的距离
}
/*
注意先累加再更新,避免t身上有自环时,更新了dist[t]后,再累加时dist[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) puts("impossible");
else cout << t << endl;
return 0;
}
堆优化的Prim
Kruskal
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10, M = 2e5 + 10, INF = 0x3f3f3f3f;
int n, m;
int p[N]; // 并查集
struct Edge{
int a, b, w;
bool operator< (const Edge &e)const{
return w < e.w;
}
}edges[M];
int find(int x){
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
int kruskal(){
sort(edges, edges + m); // 利用sort对所有的边进行排序
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, b = edges[i].b, w = edges[i].w;
a = find(a), b = find(b);
if(a != b){ // 如果a和b不在一个集合里,合并a、b
p[a] = b;
res += w;
cnt ++;
}
}
if(cnt < n-1) return INF; // 最小生成树的边有n-1条
return res;
}
int main(){
cin >> n >> m;
for(int i = 0; i <= m; i++){
int a, b, c;
cin >> a >> b >> c;
edges[i] = {a, b, c};
}
int t = kruskal();
if(t == INF) puts("impossible");
else cout << t << endl;
return 0;
}
(5)二分图
染色法
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e5 + 10, M = N * 2;
int n, m;
int h[N], ne[M], e[M], idx;
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; // 从u开始,u染c种颜色
for(int i = h[u]; i != -1; i = ne[i]){
int j = e[i];
if(!color[j]){ // 如果j点没有染过颜色
if(!dfs(j, 3 - c)) return false; // dfs染j点为c的异种颜色
}
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]) // 如果i点没有染过颜色
if(!dfs(i, 1)){ // 如果发生冲突
flag = false;
break;
}
if(flag) puts("Yes");
else puts("No");
return 0;
}
匈牙利算法
#include <iostream>
#include <cstring>
using namespace std;
const int N = 510, M = 1e5 + 10;
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; // 设为true防止find(match[j])无限递归
if(match[j] == 0 || find(match[j])){ // 如果没有匹配过or对应匹配的点能找到下家
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); // 每次更新为false,否则除了第一次匹配的,后面的都无法挖墙脚达到最大匹配
if(find(i)) res ++;
}
cout << res;
return 0;
}
(6)欧拉回路和欧拉路径
(7)拓扑排序
// AcWing 848. 有向图的拓扑序列
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e5 + 10;
int n, m;
int h[N], e[N], ne[N], idx;
int q[N], d[N];// d存储每个点的入度
void add(int a, int b){
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
// 返回是否存在拓扑序列
bool topsort(){
int hh = 0, tt = -1;
// 入度为0的点入队
for(int i = 1; i <= n; i++)
if(!d[i]) q[++tt] = i;
while(hh <= tt){
int t = q[hh++];// 队头出队
for(int i = h[t]; i != -1; i = ne[i]){
int j = e[i];
if(--d[j] == 0) q[++tt] = j;// 入度为0的点入队
}
}
return 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()) puts("-1");
else{
for(int i = 0; i < n; i++) cout << q[i] << ' ';
}
return 0;
}
(8)网络流
四、数学知识
(1)质数
从2开始的整数开始定义。
// 判定是否为质数 - 试除法 O(sqrt(n))
bool is_prime(int n){
if(n < 2) return false;
for(int i = 2; i <= n / i; i++)
if(n % i == 0) return false;
return true;
}
// 求n的质因子 - 试除法 O(logn ~ sqrt(n))
void divide(int n){
for(int i = 2; i <= n / i; i++)
if(n % i == 0){
int s = 0;
while(n % i == 0){
n /= i;
s++;
}
cout << i << " " << s << endl; // i^s
}
if(n > 1) cout << n << " " << 1 << endl; // n中最多只包含一个>sqr t(n)的质因子
}
// 求n以内的质数及个数 - 埃氏筛法 O(nloglogn)
bool st[N]; // 标记是否筛过
int primes[N], cnt = 0; // n以内的质数,质数个数
void get_primes(int n){
for(int i = 2; i <= n; i++)
if(!st[i]){
primes[cnt ++] = i;
for(int j = i + i; j <= n; j += i) st[j] = true; // 质数的倍数是合数,把st[质数的倍数]=true
}
}
// 求n以内的质数及个数 - 线性筛法
bool st[N];
int primes[N], cnt = 0;
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;
// 当i%primes[j]==0时,primes[j]是i的最小质因子,则primes[j]也是primes[j]*i的最小质因子;把primes[j]*i筛掉
// 当ii%primes[j]!=0时,primes[j]<i的最小质因子,则primes[j]也是primes[j]*i的最小质因子
// 合数一定存在一个最小质因子,让合数只会被最小质因子筛掉,且只被筛一次
}
}
}
(2)约数
// 求n的所有约数 - 试除法
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); // 约数i和n/i成对出现
}
}
sort(res.begin(), res.end());
return res;
}
// 求n的约数个数、约数和 - 试除法
// 分解质因数n=p1^a1 * p2^a2 * ... * pn^an
// 约数的个数=(a1+1)(a2+1)...(an+1)
unordered_map<int, int> primes;
while(n--){
cin >> a;
for(int i = 2; i <= a/i; i++){
while(a % i == 0){
a /= i;
primes[i] ++;
}
}
if(a > 1) primes[a] ++;
}
// 约数的和=(p1^0 + p1^1 + ... + p1^a1)...(pn^0 + pn^1 + ... + pn^an)
for(auto prime : primes){
int p = prime.first, a = prime.second;
LL t = 1;
while(a --) t = (t * p + 1) % mod; // 秦九韶算法,计算pn^0 + pn^1 + ... + pn^an
res = res * t % mod;
}
// 求a和b的最大公约数 - 欧几里得算法(辗转相除法)
// a%d=0且b%d=0,则(ax+by)%d=0;gcd(a,b)=gcd(b,a%b)=gcd(b,a-cb)
int gcd(int a, int b){
return b ? gcb(b, a % b) : a; // a和0的最大公约数是a
}
(3)欧拉函数
(4)快速幂
(5)扩展欧几里得算法
(6)中国剩余定理
(7)高斯消元
(8)组合计数
(9)容斥原理
(10)简单博弈论
五、动态规划
(1)背包问题
01背包
每个物品只有一个,最多选一次
// 二维
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int v, w;
int f[N][N]; // f[i][j]表示 只选前i个物品 且 总体积<=j 的最大总价值
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i++){ // f[0][?]都为0,所以i从1开始
cin >> v >> w;
for(int j = 0; j <= m; j++){
f[i][j] = f[i - 1][j]; // 不含第i个物品的情况
if(j >= v) f[i][j] = max(f[i][j], f[i - 1][j - v] + w); // 如果含第i个物品的情况存在的话,比较不含、含第i个物品的情况
}
}
cout << f[n][m];
return 0;
}
// 一维优化
// 利用滚动数组
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int v, w;
int f[N]; // 总体积<=j的最大总价值
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i++){
cin >> v >> w;
for(int j = m; j >= v; j--)
f[j] = max(f[j], f[j - v] + w);
}
cout << f[m] << endl;
return 0;
}
完全背包
每个物品有无限个,可以选无限次
// 三维
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int v, w;
int f[N][N];
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++){
cin >> v >> w;
for(int j = 0; j <= m; j ++)
for(int k = 0; k * v <= j; k ++) // 选k个第i个物品
f[i][j] = max(f[i][j], f[i - 1][j - k * v] + k * w); // 比较f[i-1][j-k*v]+k*w中的最大值
}
cout << f[n][m];
return 0;
}
// 二维优化
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int v, w;
int f[N][N];
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++){
cin >> v >> w;
for(int j = 0; j <= m; j ++){
f[i][j] = f[i - 1][j];
if(j >= v) f[i][j] = max(f[i][j], f[i][j - v] + w);
}
}
cout << f[n][m];
return 0;
}
// 一维优化
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
int v, w;
int f[N];
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++){
cin >> v >> w;
for(int j = v; j <= m; j ++)
f[j] = max(f[j], f[j - v] + w);
}
cout << f[m];
return 0;
}
多重背包
每个物品有Si个,最多选Si次
#include <iostream>
using namespace std;
const int N = 110;
int n, m;
int v, w, s;
int f[N][N];
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++){
cin >> v >> w >> s;
for(int j = 0; j <= m; j ++)
for(int k = 0; k <= s && k * v <= j; k ++)
f[i][j] = max(f[i][j], f[i - 1][j - k * v] + k * w);
}
cout << f[n][m];
return 0;
}
// 二进制优化
#include <iostream>
using namespace std;
const int N = 12010, M = 2010; // N物品数=1000*log2000+10;M体积
int n, m;
int v[N], w[N];
int f[M];
int main(){
cin >> n >> m;
int cnt = 0; // 标记打包下标
for(int i = 1; i <= n; i ++){
int a, b, s;
cin >> a >> b >> s;
int k = 1; // 从每个包里装1个物品开始打包
while(k <= s){ // 每次把k个物品装成一个01背包
v[++cnt] = a * k;
w[cnt] = b * k;
s -= k;
k *= 2;
}
if(s > 0){ // 还剩c=s个没装
v[++cnt] = a * s;
w[cnt] = b * s;
}
}
n = cnt;
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];
return 0;
}
分组背包
每组物品有Si个,最多选一个
#include <iostream>
using namespace std;
const int N = 110;
int n, m;
int v[N][N], w[N][N], s[N];
int f[N];
int main(){
cin >> n >> m;
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 = m; j >= 0; j --)
for(int k = 0; k < s[i]; k++)
if(j >= v[i][k]) // 不能写在j循环里,因为v[i][k]随着k改变而动态变化
f[j] = max(f[j], f[j-v[i][k]]+w[i][k]);
cout << f[m];
return 0;
}
(2)线性DP
// AcWing 898. 数字三角形
#include <iostream>
using namespace std;
const int N = 510, INF = 1e9;
int n;
int a[N][N];
int f[N][N];
int main(){
cin >> n;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= i; j++)
cin >> a[i][j];
for(int i = 0; i <= n; i ++)
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++)
f[i][j] = max(f[i-1][j-1] + a[i][j], f[i-1][j] + a[i][j]); // 来自左上角or来自右上角
int res = -INF;
for(int i = 1; i <= n; i++) res = max(res, f[n][i]); // 遍历最后一行找最大路径
printf("%d", res);
return 0;
}
// AcWing 895. 最长上升子序列
#include <iostream>
using namespace std;
const int N = 1010;
int n;
int a[N], f[N]; // 以a[i]结尾的子序列长度
int main(){
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 1; i <= n; i++){
f[i] = 1; // 只有a[i]一个数
for(int j = 1; j < i; j++) // 从a[1]~a[i-1]找<a[i]的数
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;
return 0;
}
// AcWing 897. 最长公共子序列
#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N]; // 所有由第一个序列的前i个字母、第二个序列的前j个字母构成的公共子序列个数
int main(){
scanf("%d%d", &n, &m);
scanf("%s%s", 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]); // f[i-1][j]和f[i][j-1],它们都包含f[i-1][j-1]的情况
if(a[i] == b[j]) f[i][j] = max(f[i][j], f[i-1][j-1] + 1);
}
printf("%d", f[n][m]);
return 0;
}
(3)区间DP
// AcWing 282. 石子合并
#include <iostream>
using namespace std;
const int N = 310, INF = 1e8;
int n;
int s[N];
int f[N][N];
int main(){
scanf("%d", &n);
for(int i = 1; i <= n; i++) scanf("%d", &s[i]);
for(int i = 1; i <= n; i++) s[i] += s[i-1]; // 计算前缀和
for(int len = 2; len <= n; len++) // 枚举区间长度
for(int l = 1; l + len - 1 <= n; l++){ // 枚举左端点
int r = l + len -1;
f[l][r] = INF;
for(int k = l; k < r; k++) // 枚举分界点k
f[l][r] = min(f[l][r], f[l][k] + f[k+1][r] + s[r] - s[l-1]);
}
printf("%d", f[1][n]);
return 0;
}
(4)计数类DP
(5)数位统计DP
// AcWing 338. 计数问题
(6)状态压缩DP
(7)树形DP
(8)记忆化搜索
六、贪心
更多推荐
所有评论(0)