第一讲:基础算法
快速排序
785.快速排序
给定你一个长度为 n 的整数数列。
请你使用快速排序对这个数列按照从小到大进行排序。
并将排好序的数列按顺序输出。
输入格式
输入共两行,第一行包含整数 n。
第二行包含 n 个整数(所有整数均在1∼10^9 范围内),表示整个数列。
输出格式
输出共一行,包含 n 个整数,表示排好序的数列。
数据范围
1≤n≤100000
输入样例:
5
3 1 2 4 5
输出样例:
1 2 3 4 5
解题思路:
模板代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=1e5+10;
int q[N];
void quick_sort(int q[],int l,int r){
if(l>=r) return; // 容易忘记要判断l>=r
int i=l-1;
int j=r+1;
int x=q[l+r>>1];
while(i<j){
do(i++); while(q[i]<x);
do(j--); while(q[j]>x);
if(i<j) swap(q[i],q[j]); // 容易忘记要再次比较i<j
}
quick_sort(q,l,j);
quick_sort(q,j+1,r);
}
int main(){
int n;
cin>>n;
for(int i=0;i<n;i++)
cin>>q[i];
quick_sort(q,0,n-1);
for(int i=0;i<n;i++)
cout<<q[i]<<" ";
return 0;
}
786.第k个数
给定一个长度为 n 的整数数列,以及一个整数 k,请用快速选择算法求出数列从小到大排序后的第 k 个数。
输入格式
第一行包含两个整数 n 和 k。
第二行包含 n 个整数(所有整数均在 1∼109 范围内),表示整数数列。
输出格式
输出一个整数,表示数列的第 k 小数。
数据范围
1≤n≤100000,
1≤k≤n
输入样例:
5 3
2 4 1 5 3
输出样例:
3
解题思路:
在进行一轮快速排序后,q[l~j] 中的所有元素都小于等于q[j+1,r]中的任意一个元素。
尽管左右两部分是乱序的,如果左边部分的元素个数小于k, 那么我们只需要对右边数组进行快排,因为第k小的数一定不在左边数组中;
同理,如果左边部分的元素个数大于或者等于k, 那么我们只需要对左边数组进行快排,因为第k小的数一定在左边数组中。
模板代码:
#include <iostream>
using namespace std;
const int N = 100010;
int q[N];
int quick_sort(int q[], int l, int r, int k)
{
if (l >= r) return q[l];
int i = l - 1, j = r + 1, x = q[l + r >> 1];
while (i < j)
{
do i ++ ; while (q[i] < x);
do j -- ; while (q[j] > x);
if (i < j) swap(q[i], q[j]);
}
if (j - l + 1 >= k) return quick_sort(q, l, j, k);
else return quick_sort(q, j + 1, r, k - (j - l + 1));
}
int main()
{
int n, k;
scanf("%d%d", &n, &k);
for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);
cout << quick_sort(q, 0, n - 1, k) << endl;
return 0;
}
归并排序
787. 归并排序
给定你一个长度为 n 的整数数列。
请你使用归并排序对这个数列按照从小到大进行排序。
并将排好序的数列按顺序输出。
输入格式
输入共两行,第一行包含整数 n。
第二行包含 n 个整数(所有整数均在 1∼109 范围内),表示整个数列。
输出格式
输出共一行,包含 n 个整数,表示排好序的数列。
数据范围
1≤n≤100000
输入样例:
5
3 1 2 4 5
输出样例:
1 2 3 4 5
解题思路:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=1e5+10;
int q[N],tmp[N];
void merge_sort(int q[],int l,int r){
if(l>=r) return; // 递归判断的结束条件
int i=l,mid=l+r>>1,j=mid+1;
// 对左边和右边的数组递归的进行归并排序
merge_sort(q,l,mid);
merge_sort(q,mid+1,r);
// 合并左边和右边的数组
int t=0;
while(i<=mid && j<=r){
if(q[i]<q[j])
tmp[t++]=q[i++];
else
tmp[t++]=q[j++];
}
while(i<=mid) tmp[t++]=q[i++];
while(j<=r) tmp[t++]=q[j++];
// 把tmp数组中的元素复制到q数组中
for(int i=l,j=0;i<=r;i++,j++) // 注意范围q是l~r
q[i]=tmp[j];
}
int main(){
int n;
cin>>n;
for(int i=0;i<n;i++)
cin>>q[i];
merge_sort(q,0,n-1);
for(int i=0;i<n;i++)
cout<<q[i];
return 0;
}
788. 逆序对的数量
给定一个长度为 n 的整数数列,请你计算数列中的逆序对的数量。
逆序对的定义如下:对于数列的第 i 个和第 j 个元素,如果满足 i<j 且 a[i]>a[j],则其为一个逆序对;否则不是。
输入格式
第一行包含整数 n,表示数列的长度。
第二行包含 n 个整数,表示整个数列。
输出格式
输出一个整数,表示逆序对的个数。
数据范围
1≤n≤100000,
数列中的元素的取值范围 [1,109]。
输入样例:
6
2 3 4 5 6 1
输出样例:
5
解题思路:
利用归并排序的思路。
i、j分别指向左右两边的有序数组
q
[
l
m
i
d
]
、
q
[
m
i
d
+
1
,
r
]
q[l~mid]、q[mid+1,r]
q[l mid]、q[mid+1,r]
当
q
[
i
]
>
q
[
j
]
q[i]>q[j]
q[i]>q[j] 时,由于
q
[
i
m
i
d
]
q[i~mid]
q[i mid]数组是升序的,此时
q
[
i
m
i
d
]
q[i~mid]
q[i mid]中
m
i
d
−
i
+
1
mid-i+1
mid−i+1个元素都大于
q
[
j
]
q[j]
q[j], 而且这些元素的位置都在
q
[
j
]
q[j]
q[j]前面,因此有
m
i
d
−
i
+
1
mid-i+1
mid−i+1个逆序对。
模板代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N=1e5+10;
int q[N],tmp[N];
LL merge_sort(int q[],int l,int r){ // 数据范围可能很大,要开LL
if(l>=r) return 0; // 递归判断的结束条件
int i=l,mid=l+r>>1,j=mid+1;
// 对左边和右边的数组递归的进行归并排序
LL res=merge_sort(q,l,mid)+merge_sort(q,mid+1,r);
// 合并左边和右边的数组
int t=0;
while(i<=mid && j<=r){ // 在求逆序对时注意此处必须是<=
if(q[i]<=q[j])
tmp[t++]=q[i++];
else {
res+=mid-i+1; // 如果q[i]>q[j], 那么对于元素q[j]来说可以构成mid-i+1 个逆序对
tmp[t++]=q[j++];
}
}
while(i<=mid) tmp[t++]=q[i++];
while(j<=r) tmp[t++]=q[j++];
// 把tmp数组中的元素复制到q数组中
for(int i=l,j=0;i<=r;i++,j++)
q[i]=tmp[j];
return res;
}
int main(){
int n;
cin>>n;
for(int i=0;i<n;i++)
cin>>q[i];
cout<<merge_sort(q,0,n-1)<<endl;
return 0;
}
二分
789. 数的范围
给定一个按照升序排列的长度为 n 的整数数组,以及 q 个查询。
对于每个查询,返回一个元素 k 的起始位置和终止位置(位置从 0 开始计数)。
如果数组中不存在该元素,则返回 -1 -1。
输入格式
第一行包含整数 n 和 q,表示数组长度和询问个数。
第二行包含 n 个整数(均在 1∼10000 范围内),表示完整数组。
接下来 q 行,每行包含一个整数 k,表示一个询问元素。
输出格式
共 q 行,每行包含两个整数,表示所求元素的起始位置和终止位置。
如果数组中不存在该元素,则返回 -1 -1。
数据范围
1≤n≤100000
1≤q≤10000
1≤k≤10000
输入样例:
6 3
1 2 2 3 3 4
3
4
5
输出样例:
3 4
5 5
-1 -1
解题思路:
本题是典型的整数二分
由于数组中的元素有可能重复,因此本题实际上是要找>=x的第一个数
和 <=x的最后一个数
对于查找>=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;
}
对于查找<=x的最后一个数
,二分算法如下:
int l=0,r=n-1;
while(l<r){
int mid=l+r+1>>1;
if(q[mid]<=x) l=mid;
else r=mid-1;
}
模板题解:
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <cstring>
using namespace std;
const int N=100000;
int n,m;
int q[N];
int findLeft(int k){
int l=0,r=n-1;
while(l<r){
int mid=l+r>>1;
if(q[mid]>=k) r=mid;
else l=mid+1;
}
if(q[l]!=k) return -1;
return l;
}
int findRight(int k){
int l=0,r=n-1;
while(l<r){
int mid=l+r+1>>1;
if(q[mid]<=k) l=mid;
else r=mid-1;
}
return l;
}
int main(){
cin>>n>>m;
for(int i=0;i<n;i++){
cin>>q[i];
}
int k;
while(m--){
cin>>k;
int l=findLeft(k);
int r=findRight(k);
if(l==-1){
puts("-1 -1");
continue;
}else{
cout<<l<<" "<<r<<endl;
}
}
return 0;
}
790.数的三次方根
给定一个浮点数 n,求它的三次方根。
输入格式
共一行,包含一个浮点数 n。
输出格式
共一行,包含一个浮点数,表示问题的解。
注意,结果保留 6 位小数。
数据范围
−10000≤n≤10000
输入样例:
1000.00
输出样例:
10.000000
解题思路: 本题是小数二分,比整数二分要简单很多,没有太多的约束条件。模板如下:
但是需要注意的是,由于
l
l
l和
r
r
r都是小数,此时while循环不能直接判断
l
<
r
l<r
l<r, 而是需要改成
r
−
l
<
1
e
−
8
r-l<1e-8
r−l<1e−8
模板题解
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
int main(){
double x;
cin>>x;
double l=-100,r=100;
while(r-l>1e-8){
double mid=(l+r)/2;
if(mid*mid*mid>=x) r=mid;
else l=mid;
}
printf("%.6lf\n",l);
return 0;
}
高精度
791. 高精度加法
给定两个正整数(不含前导 0),计算它们的和。
输入格式
共两行,每行包含一个整数。
输出格式
共一行,包含所求的和。
数据范围
1≤整数长度≤100000
输入样例:
12
23
输出样例:
35
模板代码:
#include <iostream>
#include <vector>
using namespace std;
vector<int> add(vector<int> &A, vector<int> &B)
{
if (A.size() < B.size()) return add(B, A); // 保证A的位数大于B的位数
vector<int> C;
int t = 0;
for (int i = 0; i < A.size(); i ++ )
{
t += A[i]; // 当前A[i]的值加上进位数
if (i < B.size()) t += B[i];// 再加上当前B[i]的值
C.push_back(t % 10); // 余数压入C中
t /= 10; // 保存进位数
}
// 当A的最高位加完之后,可能还有进位
if (t) C.push_back(t); // !! 这个不要遗忘 !!
return C;
}
int main()
{
string a, b;
vector<int> A, B;
cin >> a >> b;
// 数字的低位保存在数组的低位,比如个位数存放到a[0]中,十位数存放在a[1]中...
for (int i = a.size() - 1; i >= 0; i -- ) A.push_back(a[i] - '0');
for (int i = b.size() - 1; i >= 0; i -- ) B.push_back(b[i] - '0');
auto C = add(A, B);
for (int i = C.size() - 1; i >= 0; i -- ) cout << C[i];
cout << endl;
return 0;
}
792. 高精度减法
给定两个正整数(不含前导 0),计算它们的差,计算结果可能为负数。
输入格式
共两行,每行包含一个整数。
输出格式
共一行,包含所求的差。
数据范围
1≤整数长度≤105
输入样例:
32
11
输出样例:
21
模板代码:
#include <iostream>
#include <stdio.h>
#include <vector>
using namespace std;
// 判断大小
bool cmp(vector<int>& A, vector<int>& B) {
if (A.size() != B.size()) // 数组长的那个数值一定更大
return A.size() > B.size();
for (int i = A.size() - 1; i >= 0; i--) // 如果两个数组相等,则从高位到低位的顺序逐次比较大小
if (A[i] != B[i])
return A[i] > B[i];
return true;
}
// 计算差值,其中保证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; // 用A[i]减去借位
if (i < B.size()) t -= B[i]; // 再减去B[i]
C.push_back((t + 10) % 10); // t有可能小于10,因此先加10,然后将余数存入C中
if (t < 0) t = 1; // 如果t<0, 则需要向前借位
else t = 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');
// 比较A和B的大小, 先调用sub函数计算两个数差值的绝对值。 如果A<B,需要添加负号
if (cmp(A, B)) C = sub(A, B);
else C = sub(B, A), cout << '-';
for (int i = C.size() - 1; i >= 0; i--)
cout << C[i];
cout << endl;
return 0;
}
793. 高精度乘法
给定两个非负整数(不含前导 0) A 和 B,请你计算 A×B 的值。
输入格式
共两行,第一行包含整数 A,第二行包含整数 B。
输出格式
共一行,包含 A×B 的值。
数据范围
1≤A的长度≤100000,
0≤B≤10000
输入样例:
2
3
输出样例:
6
模板代码:
#include <iostream>
#include <stdio.h>
#include <vector>
using namespace std;
vector<int> mul(vector<int>& A, int b) {
vector<int> C;
int t = 0;
for (int i = 0; i < A.size() || t; i++) {
if (i < A.size()) t += A[i] * b; // 将t=t+A[i]*b
C.push_back(t % 10); // 余数压入C中
t /= 10;
}
// 高位可能有前导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--) printf("%d", C[i]);
return 0;
}
794. 高精度除法
给定两个非负整数(不含前导 0) A,B,请你计算 A/B 的商和余数。
输入格式
共两行,第一行包含整数 A,第二行包含整数 B。
输出格式
共两行,第一行输出所求的商,第二行输出所求余数。
数据范围
1≤A的长度≤100000,
1≤B≤10000,
B 一定不为 0
输入样例:
7
2
输出样例:
3
1
模板代码:
#include <iostream>
#include <stdio.h>
#include <vector>
#include <algorithm>
using namespace std;
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()); // 反转
// 删除前导0
while (C.size() > 1 && C.back() == 0) C.pop_back();
return C;
}
int main() {
string a;
vector<int> A;
int B;
cin >> a >> B;
for (int i = a.size() - 1; i >= 0; i--) A.push_back(a[i] - '0');
int r;
auto C = div(A, B, r);
for (int i = C.size() - 1; i >= 0; i--) cout << C[i];
cout << endl << r << endl;
return 0;
}
前缀和差分
795. 前缀和
输入一个长度为 n 的整数序列。
接下来再输入 m 个询问,每个询问输入一对 l,r。
对于每个询问,输出原序列中从第 l 个数到第 r 个数的和。
输入格式
第一行包含两个整数 n 和 m。
第二行包含 n 个整数,表示整数数列。
接下来 m 行,每行包含两个整数 l 和 r,表示一个询问的区间范围。
输出格式
共 m 行,每行输出一个询问的结果。
数据范围
1≤l≤r≤n,
1≤n,m≤100000,
−1000≤数列中元素的值≤1000
输入样例:
5 3
2 1 3 6 4
1 2
1 3
2 4
输出样例:
3
6
10
解题思路:
模板代码:
#include <iostream>
#include <stdio.h>
#include <vector>
using namespace std;
const int N = 100010;
int n, m;
int a[N], s[N];
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]); // a[i] 保存原数组中第i个元素的值
for (int i = 1; i <= n; i++) s[i] = s[i - 1] + a[i]; // s[i] 保存数组中前i个元素的和
while (m--) {
int l, r;
scanf("%d%d", &l, &r);
printf("%d\n", s[r] - s[l - 1]);
}
return 0;
}
796. 子矩阵的和
输入一个 n 行 m 列的整数矩阵,再输入 q 个询问,每个询问包含四个整数 x1,y1,x2,y2,表示一个子矩阵的左上角坐标和右下角坐标。
对于每个询问输出子矩阵中所有数的和。
输入格式
第一行包含三个整数 n,m,q。
接下来 n 行,每行包含 m 个整数,表示整数矩阵。
接下来 q 行,每行包含四个整数 x1,y1,x2,y2,表示一组询问。
输出格式
共 q 行,每行输出一个询问的结果。
数据范围
1≤n,m≤1000,
1≤q≤200000,
1≤x1≤x2≤n,
1≤y1≤y2≤m,
−1000≤矩阵内元素的值≤1000
输入样例:
3 4 3
1 7 2 4
3 6 2 8
2 1 2 3
1 1 2 2
2 1 3 4
1 3 3 4
输出样例:
17
27
21
解题思路:
s
[
i
−
1
]
[
j
−
1
]
=
s
1
s[i-1][j-1]=s1
s[i−1][j−1]=s1
s
[
i
−
1
]
[
j
]
=
s
1
+
s
2
s[i-1][j]=s1+s2
s[i−1][j]=s1+s2
s
[
i
]
[
j
−
1
]
=
s
1
+
s
3
s[i][j-1]=s1+s3
s[i][j−1]=s1+s3
s
[
i
]
[
j
]
=
s
1
+
s
2
+
s
3
=
s
[
i
−
1
]
[
j
]
+
s
[
i
]
[
j
−
1
]
−
s
[
i
−
1
]
[
j
−
1
]
s[i][j]=s1+s2+s3=s[i-1][j]+s[i][j-1]-s[i-1][j-1]
s[i][j]=s1+s2+s3=s[i−1][j]+s[i][j−1]−s[i−1][j−1]
模板代码:
#include <iostream>
#include <stdio.h>
#include <vector>
using namespace std;
const int N = 1010;
int n, m,q;
int s[N][N];
int main() {
scanf("%d%d%d", &n, &m, &q);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
scanf("%d", &s[i][j]);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
// s[i][j] 保存左上角为(0,0),右下角为(x,y) 的矩形中的元素和
s[i][j] += s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];
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;
}
797. 差分
输入一个长度为 n 的整数序列。
接下来输入 m 个操作,每个操作包含三个整数 l,r,c,表示将序列中 [l,r] 之间的每个数加上 c。
请你输出进行完所有操作后的序列。
输入格式
第一行包含两个整数 n 和 m。
第二行包含 n 个整数,表示整数序列。
接下来 m 行,每行包含三个整数 l,r,c,表示一个操作。
输出格式
共一行,包含 n 个整数,表示最终序列。
数据范围
1≤n,m≤100000,
1≤l≤r≤n,
−1000≤c≤1000,
−1000≤整数序列中元素的值≤1000
输入样例:
6 3
1 2 2 1 2 1
1 3 1
3 5 1
1 6 1
输出样例:
3 4 5 3 4 2
解题思路:
如果每次操作都是将[l,r]中的每个元素都加上一个数c,那么时间复杂度是非常大的。
因此,我们可以考虑用差分数组的方法。
比如要在[2~5]加上数1
0011110
模板代码:
#include <iostream>
#include <stdio.h>
#include <vector>
using namespace std;
const int N = 100010;
int n, m;
int a[N], b[N];
void insert(int l, int r, int c) {
b[l] += c;
b[r + 1] -= c;
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
for (int i = 1; i <= n; i++)
insert(i, i, a[i]);
while (m--) {
int l, r, c;
scanf("%d%d%d", &l, &r, &c);
insert(l, r, c);
}
for (int i = 1; i <= n; i++)
b[i] += b[i - 1];
for (int i = 1; i <= n; i++)
printf("%d ", b[i]);
return 0;
}
798. 差分矩阵
输入一个 n 行 m 列的整数矩阵,再输入 q 个操作,每个操作包含五个整数 x1,y1,x2,y2,c,其中 (x1,y1) 和 (x2,y2) 表示一个子矩阵的左上角坐标和右下角坐标。
每个操作都要将选中的子矩阵中的每个元素的值加上 c。
请你将进行完所有操作后的矩阵输出。
输入格式
第一行包含整数 n,m,q。
接下来 n 行,每行包含 m 个整数,表示整数矩阵。
接下来 q 行,每行包含 5 个整数 x1,y1,x2,y2,c,表示一个操作。
输出格式
共 n 行,每行 m 个整数,表示所有操作进行完毕后的最终矩阵。
数据范围
1≤n,m≤1000,
1≤q≤100000,
1≤x1≤x2≤n,
1≤y1≤y2≤m,
−1000≤c≤1000,
−1000≤矩阵内元素的值≤1000
输入样例:
3 4 3
1 2 2 1
3 2 2 1
1 1 1 1
1 1 2 2 1
1 3 2 3 2
3 1 3 4 1
输出样例:
2 3 4 1
4 3 4 1
2 2 2 2
解题思路
左上角为(x1,y1), 右下角为(x2,y2)的矩阵
和上一题的差分数组类似,此题只是把一维的拓展到了二维中。
二维差分数组中
(
x
1
,
y
1
)
,
(
x
2
+
1
,
y
2
+
1
)
(x_1,y_1),(x_2+1,y_2+1)
(x1,y1),(x2+1,y2+1) 设为
c
c
c ;
(
x
2
+
1
,
y
1
)
、(
x
1
,
y
2
+
1
)
(x_2+1,y_1) 、(x_1,y_2+1)
(x2+1,y1)、(x1,y2+1)设为
−
c
-c
−c
模板代码
#include <iostream>
#include <stdio.h>
#include <vector>
using namespace std;
const int N = 1010;
int n, m, q;
int a[N][N], b[N][N];
void insert(int x1, int y1, int x2, int y2, int c) {
b[x1][y1] += c;
b[x2 + 1][y1] -= c;
b[x1][y2 + 1] -= c;
b[x2 + 1][y2 + 1] += c;
}
int main() {
scanf("%d%d%d", &n, &m, &q);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
scanf("%d", &a[i][j]);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
insert(i, j, i, j, a[i][j]);
while (q--) {
int x1, y1, x2, y2, c;
cin >> x1 >> y1 >> x2 >> y2 >> c;
insert(x1, y1, x2, y2, c);
}
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
b[i][j] += b[i - 1][j] + b[i][j - 1] - b[i - 1][j - 1];
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++)
printf("%d ", b[i][j]);
puts("");
}
return 0;
}
双指针算法
799. 最长连续不重复子序列
给定一个长度为 n 的整数序列,请找出最长的不包含重复的数的连续区间,输出它的长度。
输入格式
第一行包含整数 n。
第二行包含 n 个整数(均在 0∼105 范围内),表示整数序列。
输出格式
共一行,包含一个整数,表示最长的不包含重复的数的连续区间的长度。
数据范围
1≤n≤105
输入样例:
5
1 2 2 3 5
输出样例:
3
解题思路:
遍历数组a中的每一个元素a[i], 对于每一个i,找到j使得双指针[j, i]维护的是以a[i]结尾的最长连续不重复子序列
,长度为i - j + 1, 将这一长度与res的较大者更新给res。
对于每一个i,如何确定j的位置:由于[j, i - 1]是前一步得到的最长连续不重复子序列,所以如果[j, i]中有重复元素,一定是a[i],因此右移j直到a[i]不重复为止
模板代码
#include <iostream>
using namespace std;
const int N = 100010;
int n;
int q[N], s[N]; // 数组q用于原数组的值, 用数组s记录子序列a[j ~ i]中各元素出现次数
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);
int res = 0;
for (int i = 0, j = 0; i < n; i ++ )
{
s[q[i]] ++ ;
while (j < i && s[q[i]] > 1) s[q[j ++ ]] -- ;
res = max(res, i - j + 1);
}
cout << res << endl;
return 0;
}
800. 数组元素的目标和
给定两个升序排序的有序数组 A 和 B,以及一个目标值 x。
数组下标从 0 开始。
请你求出满足 A[i]+B[j]=x 的数对 (i,j)。
数据保证有唯一解。
输入格式
第一行包含三个整数 n,m,x,分别表示 A 的长度,B 的长度以及目标值 x。
第二行包含 n 个整数,表示数组 A。
第三行包含 m 个整数,表示数组 B。
输出格式
共一行,包含两个整数 i 和 j。
数据范围
数组长度不超过 105。
同一数组内元素各不相同。
1≤数组元素≤109
输入样例:
4 5 6
1 2 4 7
3 4 6 8 9
输出样例:
1 1
解题思路
由于数组A和B都是升序
i
i
i从0开始 从前往后遍历A
j
j
j从 m - 1开始 从后向前遍历B
和纯暴力的
O
(
n
2
)
O(n^2)
O(n2) 算法的区别就在于双指针算法中的指针
j
j
j 不会发生回退。
模板代码
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int n, m, x;
int a[N], b[N];
int main()
{
scanf("%d%d%d", &n, &m, &x);
for (int i = 0; i < n; i ++ ) scanf("%d", &a[i]);
for (int i = 0; i < m; i ++ ) scanf("%d", &b[i]);
for (int i = 0, j = m - 1; i < n; i ++ )
{
while (j >= 0 && a[i] + b[j] > x) j -- //指针j一直向前遍历,直到a[i] + b[j] 的值小于等于x
if (j >= 0 && a[i] + b[j] == x) cout << i << ' ' << j << endl; // 如果a[i] + b[j] 的值等于x,则找到目标输出
// 否则,a[i] + b[j] 的值小于x,进入下一轮循环,i指针后移,i指向的元素变大。a[i] + b[j] 的值有可能再次大于等于x
}
return 0;
}
2816. 判断子序列
给定一个长度为 n 的整数序列 a1,a2,…,an 以及一个长度为 m 的整数序列 b1,b2,…,bm。
请你判断 a 序列是否为 b 序列的子序列。
子序列指序列的一部分项按原有次序排列而得的序列,例如序列 {a1,a3,a5} 是序列 {a1,a2,a3,a4,a5} 的一个子序列。
输入格式
第一行包含两个整数 n,m。
第二行包含 n 个整数,表示 a1,a2,…,an。
第三行包含 m 个整数,表示 b1,b2,…,bm。
输出格式
如果 a 序列是 b 序列的子序列,输出一行 Yes。
否则,输出 No。
数据范围
1≤n≤m≤105,
−10^9≤ai,bi≤10^9
输入样例:
3 5
1 3 5
1 2 3 4 5
输出样例:
Yes
解题思路:
本题是判断 a 序列是否是 b 序列的子序列。
用指针
i
i
i指向a序列的头,指针
j
j
j指向b序列的头,然后从前向后遍历。
如果a[i]==b[j],说明当前元素匹配成功,i和j指针同时后移。否则匹配失败,只将j指针后移,寻找可以和a[i]相等的元素。
模板代码
#include <iostream>
#include <stdio.h>
#include <vector>
using namespace std;
const int N = 100010;
int n, m;
int a[N], b[N];
int main() {
scanf("%d%d", &n, &m);
for (int i = 0; i < n; i++) scanf("%d", &a[i]);
for (int i = 0; i < m; i++) scanf("%d", &b[i]);
int i = 0, j = 0;
while (i < n && j < m) {
if (a[i] == b[j]) i++;
j++;
}
if (i == n) puts("Yes");
else puts("No");
return 0;
}
位运算
801. 二进制中1的个数
给定一个长度为 n 的数列,请你求出数列中每个数的二进制表示中 1 的个数。
输入格式
第一行包含整数 n。
第二行包含 n 个整数,表示整个数列。
输出格式
共一行,包含 n 个整数,其中的第 i 个数表示数列中的第 i 个数的二进制表示中 1 的个数。
数据范围
1≤n≤100000,
0≤数列中元素的值≤109
输入样例:
5
1 2 3 4 5
输出样例:
1 1 2 1 2
解题思路
求n的第k位数字:n>>k&1
返回n的最后一位1:lowbit(n)=n&-n
模板代码
#include <iostream>
#include <stdio.h>
#include <vector>
using namespace std;
const int N = 100010;
int n, m;
int a[N], b[N];
int main() {
int n;
scanf("%d", &n);
while (n--) {
int x, s = 0;
scanf("%d", &x);
for (int i = x; i; i -= i & -i) s++;
printf("%d ", s);
}
return 0;
}
离散化
802. 区间和
假定有一个无限长的数轴,数轴上每个坐标上的数都是 0。
现在,我们首先进行 n 次操作,每次操作将某一位置 x 上的数加 c。
接下来,进行 m 次询问,每个询问包含两个整数 l 和 r,你需要求出在区间 [l,r] 之间的所有数的和。
输入格式
第一行包含两个整数 n 和 m。
接下来 n 行,每行包含两个整数 x 和 c。
再接下来 m 行,每行包含两个整数 l 和 r。
输出格式
共 m 行,每行输出一个询问中所求的区间内数字和。
数据范围
−10^9≤x≤10^9,
1≤n,m≤10^5,
−10^9≤l≤r≤10^9,
−10000≤c≤10000
输入样例:
3 3
1 2
3 6
7 5
1 3
4 6
7 8
输出样例:
8
0
5
解题思路
由于离散化区间非常大,如果按照常规的做法的话,时间开销非常大。
因此,我们可以对原区间的坐标轴映射到新的坐标轴上。
find
函数就是输入【映射前的下标】,返回在【alls中的下标+1】
+1的目的是为了求区间和时少一步下标为0的判断。
模板代码
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <vector>
using namespace std;
typedef pair<int, int> PII;
const int N = 300010;
int n, m;
int a[N], s[N]; // 数组a存放新的数组值,s[i] 表示数组a的前i个元素的和
vector<int> alls; // 保存在添加和查询过程中,所有用到的坐标值集合,新的坐标轴上的点
vector<PII> add, query;
// 二分法查找
int find(int x) {
int l = 0, r = alls.size() - 1;
while (l < r) {
int mid = l + r >> 1;
if (alls[mid] >= x) r = mid;
else l = mid + 1;
}
return r + 1;
}
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;
}
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;
}
区间合并
803. 区间合并
给定 n 个区间 [li,ri],要求合并所有有交集的区间。
注意如果在端点处相交,也算有交集。
输出合并完成后的区间个数。
例如:[1,3] 和 [2,6] 可以合并为一个区间 [1,6]。
输入格式
第一行包含整数 n。
接下来 n 行,每行包含两个整数 l 和 r。
输出格式
共一行,包含一个整数,表示合并区间完成后的区间个数。
数据范围
1≤n≤100000,
−109≤li≤ri≤109
输入样例:
5
1 2
2 4
5 6
7 8
7 9
输出样例:
3
模板代码
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <vector>
using namespace std;
typedef pair<int, int> PII;
void merge(vector<PII>& segs) {
vector<PII> res;
sort(segs.begin(), segs.end()); // 按照左端点进行排序
int st = -2e9, ed = -2e9;
for(auto seg:segs)
if (ed < seg.first) {
if (st != -2e9) res.push_back({ st,ed });
st = seg.first, ed = seg.second;
}
else {
ed = max(ed, seg.second);
}
if (st != -2e9) res.push_back({ st,ed });
segs = res;
}
int main() {
int n;
scanf("%d", &n);
vector<PII> segs;
for (int i = 0; i < n; i++) {
int l, r;
scanf("%d%d", &l, &r);
segs.push_back({ l,r });
}
merge(segs);
cout << segs.size() << endl;
return 0;
}
第二讲: 数据结构
单链表
826. 单链表
实现一个单链表,链表初始为空,支持三种操作:
向链表头插入一个数;
删除第 k 个插入的数后面的数;
在第 k 个插入的数后插入一个数。
现在要对该链表进行 M 次操作,进行完所有操作后,从头到尾输出整个链表。
注意:题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。
输入格式
第一行包含整数 M,表示操作次数。
接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:
H x,表示向链表头插入一个数 x。
D k,表示删除第 k 个插入的数后面的数(当 k 为 0 时,表示删除头结点)。
I k x,表示在第 k 个插入的数后面插入一个数 x(此操作中 k 均大于 0)。
输出格式
共一行,将整个链表从头到尾输出。
数据范围
1≤M≤100000
所有操作保证合法。
输入样例:
10
H 9
I 1 1
D 1
D 0
H 6
I 3 6
I 4 5
I 4 5
I 3 4
D 6
输出样例:
6 4 6 5
代码模板
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 100010;
int head, e[N], ne[N], idx;
/*
head: 头结点指向第idx个插入的元素
e[idx]: 第idx插入元素的值
ne[idx]: 第idx插入元素邻接的下一个元素
*/
void init() {
head = -1; // 头结点指针
idx = 0; // 第idx插入的结点
}
// 表示向链表头插入一个数 x
void add_to_head(int x) {
e[idx] = x, ne[idx] = head, head = idx++;
}
// 表示在第 k 个插入的数后面插入一个数 x
void add(int k, int x) {
e[idx] = x, ne[idx] = ne[k], ne[k] = idx++;
}
// 表示删除第 k 个插入的数后面的数
void remove(int k) {
ne[k] = ne[ne[k]];
}
int main() {
int m;
cin >> m;
init();
while (m--) {
int k, x;
char op;
cin >> op;
if (op == 'H') {
cin >> x;
add_to_head(x);
}
else if (op == 'D') {
cin >> k;
if (!k) head = ne[head];
else remove(k - 1);
}
else {
cin >> k >> x;
add(k - 1, x);
}
}
for (int i = head; i != -1; i = ne[i])
cout << e[i] << ' ';
cout << endl;
return 0;
}
双链表
827. 双链表
实现一个双链表,双链表初始为空,支持 5 种操作:
在最左侧插入一个数;
在最右侧插入一个数;
将第 k 个插入的数删除;
在第 k 个插入的数左侧插入一个数;
在第 k 个插入的数右侧插入一个数
现在要对该链表进行 M 次操作,进行完所有操作后,从左到右输出整个链表。
注意:题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。
输入格式
第一行包含整数 M,表示操作次数。
接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:
L x,表示在链表的最左端插入数 x。
R x,表示在链表的最右端插入数 x。
D k,表示将第 k 个插入的数删除。
IL k x,表示在第 k 个插入的数左侧插入一个数。
IR k x,表示在第 k 个插入的数右侧插入一个数。
输出格式
共一行,将整个链表从左到右输出。
数据范围
1≤M≤100000
所有操作保证合法。
输入样例:
10
R 7
D 1
L 3
IL 2 10
D 3
IL 2 7
L 8
R 9
IL 4 7
IR 2 2
输出样例:
8 7 7 3 2 9
解题思路
模板代码
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 100010;
int m;
int e[N], l[N], r[N], idx;
/*
e[idx] 表示第idx插入元素的值
l[idx] 表示第idx插入元素的左指针指向的元素
r[idx] 表示第idx插入元素的右指针指向的元素
*/
void insert(int a, int x) { //在第a个插入元素的右边插入元素x
e[idx] = x;
l[idx] = a, r[idx] = r[a];
l[r[a]] = idx, r[a] = idx++;
}
void remove(int a) { // 删除第a个插入的元素
l[r[a]] = l[a];
r[l[a]] = r[a];
}
int main() {
cin >> m;
r[0] = 1, l[1] = 0; // idx=0 的结点表示头结点,idx=1的结点表示尾结点
idx = 2;
while (m--) {
string op;
cin >> op;
int k, x;
if (op == "L") {
cin >> x;
insert(0, x); // 在idx=0的结点的右侧插入一个元素
}
else if (op == "R") {
cin >> x;
insert(l[1], x); // 在尾结点的左节点的右侧插入一个元素
}
else if (op == "D") {
cin >> k;
remove(k + 1); // 第k个插入的数,对应的idx=k+1
}
else if (op == "IL") { // 表示在第 k 个插入的数左侧插入一个数
cin >> k >> x;
insert(l[k + 1], x);
}
else { // 在第 k 个插入的数右侧插入一个数
cin >> k >> x;
insert(k + 1, x);
}
}
for (int i = r[0]; i != 1; i = r[i]) cout << e[i] << ' ';
cout << endl;
return 0;
}
栈
828. 模拟栈
实现一个栈,栈初始为空,支持四种操作:
push x – 向栈顶插入一个数 x;
pop – 从栈顶弹出一个数;
empty – 判断栈是否为空;
query – 查询栈顶元素。
现在要对栈进行 M 个操作,其中的每个操作 3 和操作 4 都要输出相应的结果。
输入格式
第一行包含整数 M,表示操作次数。
接下来 M 行,每行包含一个操作命令,操作命令为 push x,pop,empty,query 中的一种。
输出格式
对于每个 empty 和 query 操作都要输出一个查询结果,每个结果占一行。
其中,empty 操作的查询结果为 YES 或 NO,query 操作的查询结果为一个整数,表示栈顶元素的值。
数据范围
1≤M≤100000,
1≤x≤109
所有操作保证合法。
输入样例:
10
push 5
query
push 6
pop
query
pop
empty
push 4
query
empty
输出样例:
5
5
YES
4
NO
模板代码
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 100010;
int m;
int stk[N], tt;
int main() {
cin >> m;
while (m--) {
string op;
int x;
cin >> op;
if (op == "push") {
cin >> x;
stk[++tt] = x;
}
else if (op == "pop") tt--;
else if (op == "empty") cout << (tt ? "NO" : "YES") << endl;
else cout << stk[tt] << endl;
}
return 0;
}
3302. 表达式求值
给定一个表达式,其中运算符仅包含 +,-,*,/(加 减 乘 整除),可能包含括号,请你求出表达式的最终值。
注意:
数据保证给定的表达式合法。
题目保证符号 - 只作为减号出现,不会作为负号出现,例如,-1+2,(2+2)*(-(1+1)+2) 之类表达式均不会出现。
题目保证表达式中所有数字均为正整数。
题目保证表达式在中间计算过程以及结果中,均不超过 231−1。
题目中的整除是指向 0 取整,也就是说对于大于 0 的结果向下取整,例如 5/3=1,对于小于 0 的结果向上取整,例如 5/(1−4)=−1。
C++和Java中的整除默认是向零取整;Python中的整除//默认向下取整,因此Python的eval()函数中的整除也是向下取整,在本题中不能直接使用。
输入格式
共一行,为给定表达式。
输出格式
共一行,为表达式的结果。
数据范围
表达式的长度不超过 10^5。
输入样例:
(2+2)*(1+1)
输出样例:
8
解题模板
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <vector>
#include <cstring>
#include <stack>
#include <unordered_map>
using namespace std;
stack<int> num; // 数字栈
stack<char> op; // 操作符栈
//弹出左右操作数操作符进行计算,并将计算结果压入操作数栈中
void eval(){
// 根据栈先进后出的特点
auto a=num.top();num.pop(); // a是右操作数
auto b=num.top();num.pop(); // b是左操作数
auto c=op.top(); op.pop(); // c是操作符
int res;
if(c=='+') res=b+a;
else if(c=='-') res=b-a;
else if(c=='*') res=b*a;
else if(c=='/') res=b/a;
num.push(res);
}
int main(){
string exp;
cin>>exp;
// 操作符的优先级
unordered_map<char,int> pr{{'+',1},{'-',1},{'*',2},{'/',2}};
for(int i=0;i<exp.size();i++){
if(isdigit(exp[i])){ // 如果当前字符是数字
int sum=0,j;
for(j=i;isdigit(exp[j]) &&j<exp.size();j++){ // 当前数字可能有多位
sum=sum*10+exp[j]-'0';
}
num.push(sum); // 计算出数字,压入数字栈中
i=j-1;
}else if(exp[i]=='(') { // 如果是左括号,则压入符号栈中
op.push(exp[i]);
}else if(exp[i]==')'){ // 如果是有括号
while(op.top()!='('){ // 计算括号内的表达式
eval();
}
op.pop(); // 弹出与之匹配的左括号
} else{ // 如果当前遇到的是运算符
// 当操作栈中还有操作符,且栈顶的操作符不为左括号,且栈顶的操作符的优先级大于当前操作符
while(op.size() && op.top()!='(' && pr[op.top()]>=pr[exp[i]])
eval();// 先计算优先级高的操作符对应的运算
op.push(exp[i]); // 将当前操作符压入栈中
}
}
while(op.size()) eval(); // 一直计算,直到当前操作符栈为空
cout<<num.top()<<endl;
return 0;
}
队列
829. 模拟队列
实现一个队列,队列初始为空,支持四种操作:
push x – 向队尾插入一个数 x;
pop – 从队头弹出一个数;
empty – 判断队列是否为空;
query – 查询队头元素。
现在要对队列进行 M 个操作,其中的每个操作 3 和操作 4 都要输出相应的结果。
输入格式
第一行包含整数 M,表示操作次数。
接下来 M 行,每行包含一个操作命令,操作命令为 push x,pop,empty,query 中的一种。
输出格式
对于每个 empty 和 query 操作都要输出一个查询结果,每个结果占一行。
其中,empty 操作的查询结果为 YES 或 NO,query 操作的查询结果为一个整数,表示队头元素的值。
数据范围
1≤M≤100000,
1≤x≤109,
所有操作保证合法。
输入样例:
10
push 6
empty
query
pop
empty
push 3
push 4
pop
query
push 6
输出样例:
NO
6
YES
4
代码模板
#include <iostream>
#include <cstring>
#include <stack>
#include <unordered_map>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 100010;
int m;
int q[N], hh=0, tt = -1;
int main() {
cin >> m;
while (m--) {
string op;
int x;
cin >> op;
if (op == "push") {
cin >> x;
q[++tt] = x;
}
else if (op == "pop") hh++;
else if (op == "empty") cout << (hh <= tt ? "NO" : "YES") << endl;
else cout << q[hh] << endl;
}
return 0;
}
单调栈
830. 单调栈
给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。
输入格式
第一行包含整数 N,表示数列长度。
第二行包含 N 个整数,表示整数数列。
输出格式
共一行,包含 N 个整数,其中第 i 个数表示第 i 个数的左边第一个比它小的数,如果不存在则输出 −1。
数据范围
1≤N≤10^5
1≤数列中元素≤10^9
输入样例:
5
3 4 2 7 5
输出样例:
-1 3 -1 2 2
解题思路
代码模板
#include <iostream>
#include <cstring>
#include <stack>
#include <unordered_map>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 100010;
int stk[N], tt;
int main() {
int n;
cin >> n;
while (n--) {
int x;
scanf("%d", &x);
while (tt && stk[tt] >= x) tt--;
if (!tt) printf("-1 ");
else printf("%d ",stk[tt]);
stk[++tt] = x;
}
return 0;
}
单调队列
154. 滑动窗口
给定一个大小为 n≤10^6 的数组。
有一个大小为 k 的滑动窗口,它从数组的最左边移动到最右边。
你只能在窗口中看到 k 个数字。
每次滑动窗口向右移动一个位置。
以下是一个例子:
该数组为 [1 3 -1 -3 5 3 6 7],k 为 3。
你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。
输入格式
输入包含两行。
第一行包含两个整数 n 和 k,分别代表数组长度和滑动窗口的长度。
第二行有 n 个整数,代表数组的具体数值。
同行数据之间用空格隔开。
输出格式
输出包含两个。
第一行输出,从左至右,每个位置滑动窗口中的最小值。
第二行输出,从左至右,每个位置滑动窗口中的最大值。
输入样例:
8 3
1 3 -1 -3 5 3 6 7
输出样例:
-1 -3 -3 -3 3 3
3 3 5 5 6 7
模板代码
#include <iostream>
#include <cstring>
#include <stack>
#include <unordered_map>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 1000010;
int a[N]; // 存放的是原数组中的值
int q[N]; // q是单调递增的队列,存放的是数组元素的下标。
int main() {
int n, k;
scanf("%d%d", &n, &k);
for (int i = 0; i < n; i++)
scanf("%d", &a[i]);
int hh = 0, tt = -1;
for (int i = 0; i < n; i++) {
if (hh <= tt && i - k + 1 > q[hh]) hh++; // 队首出窗口, 队首出队
while (hh <= tt && a[q[tt]] >= a[i]) tt--; // 保证队列单调递增
q[++tt] = i; // 插入当前元素
// i表示此时插入的数的下标,i=k-1时,插入了第k-1个数,才得到了第一个窗口,所以i>=k-1了才开始求极值
if (i >= k - 1) printf("%d ", a[q[hh]]);
}
cout << endl;
hh = 0, tt = -1;
for (int i = 0; i < n; i++) {
if (hh <= tt && i - k + 1 > q[hh]) hh++;
while (hh <= tt && a[q[tt]] <= a[i]) tt--; // 保证队列单调递减
q[++tt] = i;
if (i >= k - 1) printf("%d ", a[q[hh]]);
}
cout << endl;
return 0;
}
KMP
831. KMP字符串
给定一个字符串 S,以及一个模式串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。
模式串 P 在字符串 S 中多次作为子串出现。
求出模式串 P 在字符串 S 中所有出现的位置的起始下标。
输入格式
第一行输入整数 N,表示字符串 P 的长度。
第二行输入字符串 P。
第三行输入整数 M,表示字符串 S 的长度。
第四行输入字符串 S。
输出格式
共一行,输出所有出现位置的起始下标(下标从 0 开始计数),整数之间用空格隔开。
数据范围
1≤N≤10^5
1≤M≤10^6
输入样例:
3
aba
5
ababa
输出样例:
0 2
模板代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=100010,M=1000010;
int n,m;
char p[N],s[M];
int ne[N];
int main(){
scanf("%d%s",&n,p+1); // 从p[1]开始存放
scanf("%d%s",&m,s+1); // 从s[1]开始存放
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;
}
for(int i=1,j=0;i<=m;i++){
while(j&& s[i]!=p[j+1]) j=ne[j];
if(s[i]==p[j+1]) j++;
if(j==n) cout<< i-n<<" ";
}
return 0;
}
Trie
835. Trie字符串统计
维护一个字符串集合,支持两种操作:
I x 向集合中插入一个字符串 x;
Q x 询问一个字符串在集合中出现了多少次。
共有 N 个操作,输入的字符串总长度不超过 105,字符串仅包含小写英文字母。
输入格式
第一行包含整数 N,表示操作数。
接下来 N 行,每行包含一个操作指令,指令为 I x 或 Q x 中的一种。
输出格式
对于每个询问指令 Q x,都要输出一个整数作为结果,表示 x 在集合中出现的次数。
每个结果占一行。
数据范围
1≤N≤2∗104
输入样例:
5
I abc
Q abc
Q ab
I ab
Q ab
输出样例:
1
0
1
解题思路
模板代码
#include <iostream>
#include <cstring>
#include <stack>
#include <unordered_map>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 100010;
int son[N][26]; // 因为一共有26个字母,因此每个结点最多有26个儿子结点, 如果其值为1,表明第p个结点的第i个儿子存在,否则不存在。
int cnt[N]; // 以当前字符结尾的字符串的个数
int idx; // 结点标号(小标是0的点,既是根节点,也可能是空节点)
char str[N];
// 当想插入一个字符串str时
void insert(char* str) {
int p = 0; // 从根节点p=0开始遍历
for (int i = 0; str[i]; i++) {
int u = str[i] - 'a';
// 当第u个儿子不存在时,则创建该结点
if (!son[p][u]) son[p][u] = ++idx;
p = son[p][u];
}
cnt[p]++; // 以结点p结尾的字符串的个数加1
}
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; // 如果没有找到就返回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 == 'I') insert(str);
else printf("%d\n", query(str));
}
return 0;
}
143. 最大异或对
在给定的 N 个整数 A1,A2……AN 中选出两个进行 xor(异或)运算,得到的结果最大是多少?
输入格式
第一行输入一个整数 N。
第二行输入 N 个整数 A1~AN。
输出格式
输出一个整数表示答案。
数据范围
1≤N≤10^5,
0≤Ai<2^31
输入样例:
3
1 2 3
输出样例:
3
模板代码
#include <iostream>
#include <cstring>
#include <stack>
#include <unordered_map>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 100010, M = 3100010;
int n;
int a[N]; // 存放原数据的数组
int son[M][2]; // 因为只可能取到1或者0,每个结点最多有两个儿子结点
// 例如son[i][0]=1 表示标号为i的结点存在0儿子;反之如果son[i][0]=0 表示标号为i的结点不存在0儿子
int idx; // 结点标号
// 把数字转换成二进制的形式并插入Trie树中
void insert(int x) {
int p = 0;
for (int i = 30; i >= 0; i--) { // 由于一个数的二进制表示最多有31位,
int& s = son[p][x >> i & 1];
if (!s) s = ++idx;
p = s;
}
}
// 寻找每一位都和数x相反的数,如果数x的第i位为1,那么在Trie树中寻找数为0的儿子结点。
int search(int x) {
int p = 0, res = 0;
for (int i = 30; i >= 0; i--) {
int s = x >> i & 1;
if (son[p][!s]) { // 如果存在相反的数
res += 1 << i; // 则异或的结果加1
p = son[p][!s];
}
else p = son[p][s];
}
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, search(a[i]));
printf("%d\n", res);
return 0;
}
并查集
836. 合并集合
一共有 n 个数,编号是 1∼n,最开始每个数各自在一个集合中。
现在要进行 m 个操作,操作共有两种:
M a b,将编号为 a 和 b 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
Q a b,询问编号为 a 和 b 的两个数是否在同一个集合中;
输入格式
第一行输入整数 n 和 m。
接下来 m 行,每行包含一个操作指令,指令为 M a b 或 Q a b 中的一种。
输出格式
对于每个询问指令 Q a b,都要输出一个结果,如果 a 和 b 在同一集合内,则输出 Yes,否则输出 No。
每个结果占一行。
数据范围
1≤n,m≤10^5
输入样例:
4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4
输出样例:
Yes
No
Yes
模板代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=1e5+10;
int p[N];
int find(int x){ // 寻找结点x所在集合的根节点
if(p[x]!=x) p[x]=find(p[x]);
return p[x];
}
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
p[i]=i; // 初始时,每个结点都是一个独立的集合
while(m--){
char op[2];
int a,b;
cin>>op>>a>>b;
if(*op=='M')
p[find(a)]=find(b); // 合并操作就是把该结点(a)所在集合的父结点(find(a)) 的父亲结点设置为b结点所在集合的根节点
else{
if(find(a)==find(b))
puts("Yes");
else
puts("No");
}
}
return 0;
}
837. 连通块中点的数量
给定一个包含 n 个点(编号为 1∼n)的无向图,初始时图中没有边。
现在要进行 m 个操作,操作共有三种:
C a b,在点 a 和点 b 之间连一条边,a 和 b 可能相等;
Q1 a b,询问点 a 和点 b 是否在同一个连通块中,a 和 b 可能相等;
Q2 a,询问点 a 所在连通块中点的数量;
输入格式
第一行输入整数 n 和 m。
接下来 m 行,每行包含一个操作指令,指令为 C a b,Q1 a b 或 Q2 a 中的一种。
输出格式
对于每个询问指令 Q1 a b,如果 a 和 b 在同一个连通块中,则输出 Yes,否则输出 No。
对于每个询问指令 Q2 a,输出一个整数表示点 a 所在连通块中点的数量
每个结果占一行。
数据范围
1≤n,m≤105
输入样例:
5 5
C 1 2
Q1 1 2
Q2 1
C 2 5
Q2 5
输出样例:
Yes
2
3
模板代码
#include <iostream>
#include <cstring>
#include <stack>
#include <unordered_map>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 100010;
int n, m;
int p[N], cnt[N]; // cnt保存该节点所在集合中元素的个数
int find(int x) {
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
p[i] = i;
cnt[i] = 1;
}
while (m--) {
string op;
int a, b;
cin >> op;
if (op == "C") {
cin >> a >> b;
a = find(a), b = find(b);
if (a != b) {
p[a] = b;
cnt[b] += cnt[a];
}
}
else if (op == "Q1") {
cin >> a >> b;
if (find(a) == find(b)) puts("Yes");
else puts("No");
}
else {
cin >> a;
cout << cnt[find(a)] << endl;
}
}
return 0;
}
堆
838. 堆排序
输入一个长度为 n 的整数数列,从小到大输出前 m 小的数。
输入格式
第一行包含整数 n 和 m。
第二行包含 n 个整数,表示整数数列。
输出格式
共一行,包含 m 个整数,表示整数数列中前 m 小的数。
数据范围
1≤m≤n≤10^5,
1≤数列中元素≤10^9
输入样例:
5 3
4 5 1 3 2
输出样例:
1 2 3
解题思路
小根堆:每个点都小于等于左右子树,因此根节点是最小值。
手写堆:逻辑结构是二叉树,物理存储采用的是数组。根节点的下标是1。若一个结点的下标是x,左儿子是2x,右儿子是2x+1。
模板代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N=1e5+10;
int n,m;
int h[N],cnt;
void down(int u){
int t=u; // t指向结点u以及它的两个儿子中数值最小的结点
if(u*2<=cnt && h[u*2]<h[t]) t=u*2; // 如果u的左儿子存在,且左儿子的值小于u,则t指向左儿子
if(u*2+1<=cnt && h[u*2+1]<h[t]) t=u*2+1; // 如果u的右儿子存在,且右儿子的值小于u,则t指向右儿子
if(u!=t){ // 如果t指向的是u的其中一个孩子结点, 则交换两者的值
swap(h[u],h[t]);
down(t);
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>h[i];
cnt=n;
for(int i=n/2;i;i--) down(i); // 将原始堆进行排序
while(m--){
cout<<h[1]; // 输出根节点
h1=h[cnt--]; // 让最后一个结点的值覆盖根节点
down(1); // 根节点执行down操作
}
puts("");
return 0;
}
839. 模拟堆
维护一个集合,初始时集合为空,支持如下几种操作:
I x,插入一个数 x;
PM,输出当前集合中的最小值;
DM,删除当前集合中的最小值(数据保证此时的最小值唯一);
D k,删除第 k 个插入的数;
C k x,修改第 k 个插入的数,将其变为 x;
现在要进行 N 次操作,对于所有第 2 个操作,输出当前集合的最小值。
输入格式
第一行包含整数 N。
接下来 N 行,每行包含一个操作指令,操作指令为 I x,PM,DM,D k 或 C k x 中的一种。
输出格式
对于每个输出指令 PM,输出一个结果,表示当前集合中的最小值。
每个结果占一行。
数据范围
1≤N≤10^5
−10^9≤x≤10^9
数据保证合法。
输入样例:
8
I -10
PM
I -10
D 1
C 2 8
I 6
PM
DM
输出样例:
-10
6
模板代码
#include <iostream>
#include <algorithm>
#include <string.h>
using namespace std;
const int N = 100010;
int h[N]; // 用数组存放堆
int ph[N]; // ph[i]=x 表示第i个插入的元素对应的是h[x]
int hp[N];// hp[i]=x 表示h[i]中的元素对应的是第x次插入的结点
int cnt; // 堆中元素的个数
// 交换两个结点a和b
void heap_swap(int a, int b)
{
swap(ph[hp[a]],ph[hp[b]]);
swap(hp[a], hp[b]);
swap(h[a], h[b]);
}
// down操作
void down(int u)
{
int t = u;
if (u * 2 <= cnt && h[u * 2] < h[t]) t = u * 2;
if (u * 2 + 1 <= cnt && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
if (u != t)
{
heap_swap(u, t);
down(t);
}
}
// up 操作
void up(int u)
{
while (u / 2 && h[u] < h[u / 2])
{
heap_swap(u, u / 2);
u >>= 1;
}
}
int main()
{
int n, m = 0; // m表示第m个插入的元素
scanf("%d", &n); // 执行n次操作
while (n -- )
{
char op[5];
int k, x;
scanf("%s", op);
// 插入一个数 x
if (!strcmp(op, "I"))
{
scanf("%d", &x);
cnt ++ ;
m ++ ;
ph[m] = cnt, hp[cnt] = m;
h[cnt] = x;
up(cnt);
}
// 输出当前集合中的最小值(堆顶元素)
else if (!strcmp(op, "PM")) printf("%d\n", h[1]);
// 删除当前集合中的最小值(删除堆顶元素)
else if (!strcmp(op, "DM"))
{
heap_swap(1, cnt);
cnt -- ;
down(1);
}
// 删除第 k 个插入的数
else if (!strcmp(op, "D"))
{
scanf("%d", &k);
k = ph[k];
heap_swap(k, cnt);
cnt -- ;
up(k);
down(k);
}
// 修改第 k 个插入的数,将其变为 x
else
{
scanf("%d%d", &k, &x);
k = ph[k];
h[k] = x;
up(k);
down(k);
}
}
return 0;
}
哈希表
840. 模拟散列表
维护一个集合,支持如下几种操作:
I x,插入一个数 x;
Q x,询问数 x 是否在集合中出现过;
现在要进行 N 次操作,对于每个询问操作输出对应的结果。
输入格式
第一行包含整数 N,表示操作数量。
接下来 N 行,每行包含一个操作指令,操作指令为 I x,Q x 中的一种。
输出格式
对于每个询问指令 Q x,输出一个询问结果,如果 x 在集合中出现过,则输出 Yes,否则输出 No。
每个结果占一行。
数据范围
1≤N≤105
−10^9≤x≤10^9
输入样例:
5
I 1
I 2
I 3
Q 2
Q 5
输出样例:
Yes
No
模板代码
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N=1e5+3;
int h[N]; //h[i]=x 表示键值为i的头结点指向的结点标号
int e[N]; // e[i]=x表示编号为i的结点对应的真实值
int ne[N];// ne[i]=x 表示编号为i的结点连接的下一个结点编号为x
int idx; // 结点标号
// 键值相同的元素构成一个单链表,h[i]是键值为i的单链表的头结点;insert函数采用的是头插法
void insert(int x){
int k=(x%N+N)%N; // x对应的键值
e[idx]=x;
ne[idx]=h[k];
h[k]=idx++;
}
bool find(int x){
int k=(x%N+N)%N;
for(int i=h[k];i!=-1;i=ne[i]){ // 遍历单链表
if(e[i]==x)
return true;
}
return false;
}
int main() {
int n;
cin>>n;
memset(h,-1,sizeof h);
for(int i=0;i<n;i++){
string op;
int x;
cin>>op>>x;
if(op=="I"){
insert(x);
}else if(op=="Q"){
if(find(x)){
cout<< "Yes"<<endl;
}else{
cout<<"No"<<endl;
}
}
}
return 0;
}
841. 字符串哈希
给定一个长度为 n 的字符串,再给定 m 个询问,每个询问包含四个整数 l1,r1,l2,r2,请你判断 [l1,r1] 和 [l2,r2] 这两个区间所包含的字符串子串是否完全相同。
字符串中只包含大小写英文字母和数字。
输入格式
第一行包含整数 n 和 m,表示字符串长度和询问次数。
第二行包含一个长度为 n 的字符串,字符串中只包含大小写英文字母和数字。
接下来 m 行,每行包含四个整数 l1,r1,l2,r2,表示一次询问所涉及的两个区间。
注意,字符串的位置从 1 开始编号。
输出格式
对于每个询问输出一个结果,如果两个字符串子串完全相同则输出 Yes,否则输出 No。
每个结果占一行。
数据范围
1≤n,m≤105
输入样例:
8 3
aabbaabb
1 3 5 7
1 3 6 8
1 2 1 2
输出样例:
Yes
No
Yes
解题思路:
模板代码
#include <iostream>
#include <algorithm>
using namespace std;
typedef unsigned long long ULL;
const int N = 100010, P = 131;
int n, m;
char str[N];
ULL h[N], p[N];
ULL get(int l, int r)
{
return h[r] - h[l - 1] * p[r - l + 1];
}
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];
p[i] = p[i - 1] * P; // P[i]=p^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;
}
第三讲:搜索和图论
DFS
842. 排列数字
给定一个整数 n,将数字 1∼n 排成一排,将会有很多种排列方法。
现在,请你按照字典序将所有的排列方法输出。
输入格式
共一行,包含一个整数 n。
输出格式
按字典序输出所有排列方案,每个方案占一行。
数据范围
1≤n≤7
输入样例:
3
输出样例:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
解题思路

模板代码
#include <iostream>
#include <cstring>
#include <stack>
#include <unordered_map>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 10;
int n;
int path[N]; // 用于保存当前的排列结果
bool st[N]; //st[i]表示编号为i的点是否已经选过,如果st[i]=1表示已经加入到当排列中
void dfs(int u) { // 试探 path[u] 存放哪个点
if (u == n) { // n个点已经全部排列好了,打印输出
for (int i = 0; i < n; i++) printf("%d ", path[i]);
puts("");
return;
}
for(int i=1;i<=n;i++)
if (!st[i]) { // 遍历其他没有加入当前排列的结点
path[u] = i; // 排列中的第u个位置放 i
st[i] = true;
dfs(u + 1); // 递归试探path[u+1]
st[i] = false;
}
}
int main() {
scanf("%d", &n);
dfs(0);
return 0;
}
843. n-皇后问题
n−皇后问题是指将 n 个皇后放在 n×n 的国际象棋棋盘上,使得皇后不能相互攻击到,即任意两个皇后都不能处于同一行、同一列或同一斜线上。
现在给定整数 n,请你输出所有的满足条件的棋子摆法。
输入格式
共一行,包含整数 n。
输出格式
每个解决方案占 n 行,每行输出一个长度为 n 的字符串,用来表示完整的棋盘状态。
其中 . 表示某一个位置的方格状态为空,Q 表示某一个位置的方格上摆着皇后。
每个方案输出完成后,输出一个空行。
注意:行末不能有多余空格
输出方案的顺序任意,只要不重复且没有遗漏即可。
数据范围
1≤n≤9
输入样例:
4
输出样例:
.Q..
...Q
Q...
..Q.
..Q.
Q...
...Q
.Q..
模板代码
#include <iostream>
#include <cstring>
#include <stack>
#include <unordered_map>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 20;
int n;
char g[N][N];
bool col[N], dg[N], udg[N];
void dfs(int u) {
if (u == n) {
for (int i = 0; i < n; i++) puts(g[i]);
puts("");
return;
}
int x = u;
for(int y=0;y<n;y++)
if (col[y] == false && dg[y - x + n] == false && udg[y + x] == false) {
col[y] = dg[y - x + n] = udg[y + x] = true;
g[x][y] = 'Q';
dfs(x + 1);
g[x][y] = '.';
col[y] = dg[y - x + n] = udg[y + x] = false;
}
}
int main() {
cin >> n;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
g[i][j] = '.';
dfs(0);
}
BFS
844. 走迷宫
给定一个 n×m 的二维整数数组,用来表示一个迷宫,数组中只包含 0 或 1,其中 0 表示可以走的路,1 表示不可通过的墙壁。
最初,有一个人位于左上角 (1,1) 处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。
请问,该人从左上角移动至右下角 (n,m) 处,至少需要移动多少次。
数据保证 (1,1) 处和 (n,m) 处的数字为 0,且一定至少存在一条通路。
输入格式
第一行包含两个整数 n 和 m。
接下来 n 行,每行包含 m 个整数(0 或 1),表示完整的二维数组迷宫。
输出格式
输出一个整数,表示从左上角移动至右下角的最少移动次数。
数据范围
1≤n,m≤100
输入样例:
5 5
0 1 0 0 0
0 1 0 1 0
0 0 0 0 0
0 1 1 1 0
0 0 0 1 0
输出样例:
8
模板代码
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
typedef pair<int, int> PII;
const int N = 110;
int n, m;
int g[N][N], d[N][N];
int bfs() {
queue<PII> q;
memset(d, -1, sizeof d);
d[0][0] = 0;
q.push({ 0,0 });
int dx[4] = { -1,0,1,0 }, dy[4] = { 0,1,0,-1 };
while (q.size()) {
auto t = q.front();
q.pop();
for (int i = 0; i < 4; i++) {
int x = t.first + dx[i], y = t.second + dy[i];
if (x >= 0 && x < n && y >= 0 && y < m && g[x][y] == 0 && d[x][y] == -1) {
d[x][y] = d[t.first][t.second] + 1;
q.push({ x,y });
}
}
}
return d[n - 1][m - 1];
}
int main() {
cin >> n >> m;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
cin >> g[i][j];
cout << bfs() << endl;
return 0;
}
845. 八数码
在一个 3×3 的网格中,1∼8 这 8 个数字和一个 x 恰好不重不漏地分布在这 3×3 的网格中。
例如:
1 2 3
x 4 6
7 5 8
在游戏过程中,可以把 x 与其上、下、左、右四个方向之一的数字交换(如果存在)。
我们的目的是通过交换,使得网格变为如下排列(称为正确排列):
1 2 3
4 5 6
7 8 x
例如,示例中图形就可以通过让 x 先后与右、下、右三个方向的数字交换成功得到正确排列。
交换过程如下:
1 2 3 1 2 3 1 2 3 1 2 3
x 4 6 4 x 6 4 5 6 4 5 6
7 5 8 7 5 8 7 x 8 7 8 x
现在,给你一个初始网格,请你求出得到正确排列至少需要进行多少次交换。
输入格式
输入占一行,将 3×3 的初始网格描绘出来。
例如,如果初始网格如下所示:
1 2 3
x 4 6
7 5 8
则输入为:1 2 3 x 4 6 7 5 8
输出格式
输出占一行,包含一个整数,表示最少交换次数。
如果不存在解决方案,则输出 −1。
输入样例:
2 3 4 1 5 x 7 6 8
输出样例
19
模板代码
#include <stdio.h>
#include <iostream>
#include <cstring>
#include <queue>
#include <unordered_map>
#include <algorithm>
using namespace std;
int dx[4]={0,1,0,-1},dy[4]={1,0,-1,0};
int bfs(string start){
queue<string> q;
unordered_map<string,int> d; // (状态,步数)
d[start]=0;
q.push(start);
while(q.size()) {
auto t=q.front();
q.pop(); // 队头元素出队
int distance=d[t];
if(t=="12345678x") return d[t]; // 到达终态
int pos=t.find('x');
int x=pos/3,y=pos%3; // 找到x的横纵坐标
for(int i=0;i<4;i++){
int xx=x+dx[i],yy=y+dy[i];
int pp=xx*3+yy;
if(xx>=0&&xx<=2&&yy>=0 && yy<=2 ){
swap(t[pp],t[pos]); // 交换两个元素
if(!d.count(t)){
d[t]=distance+1;
q.push(t);
}
swap(t[pp],t[pos]);
}
}
}
return -1;
}
int main(){
char ch;
string start;
for(int i=0;i<9;i++){
cin>>ch;
start+=ch;
}
cout<<bfs(start)<<endl;
return 0;
}
树和图的深度优先遍历
846. 树的重心
给定一颗树,树中包含 n 个结点(编号 1∼n)和 n−1 条无向边。
请你找到树的重心,并输出将重心删除后,剩余各个连通块中点数的最大值。
重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。
输入格式
第一行包含整数 n,表示树的结点数。
接下来 n−1 行,每行包含两个整数 a 和 b,表示点 a 和点 b 之间存在一条边。
输出格式
输出一个整数 m,表示将重心删除后,剩余各个连通块中点数的最大值。
数据范围
1≤n≤105
输入样例
9
1 2
1 7
1 4
2 8
2 5
4 3
3 9
4 6
输出样例:
4
解题思路
模板代码
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
#include <unordered_map>
using namespace std;
const int N = 100010, M = 2 * N; //以有向图的格式存储无向图,所以每个节点至多对应2n-2条边
int n;
int h[N], e[M], ne[M], idx;
int ans = N;
bool st[N];
int dfs(int u) {
st[u] = true;
int size = 0, sum = 0;
for (int i = h[u]; i != -1; i = ne[i]) {
int j = e[i];
if (st[j]) continue;
int s = dfs(j);
size = max(size, s); // 将这个点删除后,剩余各个连通块中点数的最大值
sum += s;
}
size = max(size, n - sum - 1);
ans = min(ans, size);
return sum + 1;
}
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
int main() {
scanf("%d", &n);
memset(h, -1, sizeof h);
for (int i = 0; i < n - 1; i++) {
int a, b;
scanf("%d%d", &a, &b);
add(a, b), add(b, a);
}
dfs(1);
printf("%d\n", ans);
return 0;
}
847. 图中点的层次
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环。
所有边的长度都是 1,点的编号为 1∼n。
请你求出 1 号点到 n 号点的最短距离,如果从 1 号点无法走到 n 号点,输出 −1。
输入格式
第一行包含两个整数 n 和 m。
接下来 m 行,每行包含两个整数 a 和 b,表示存在一条从 a 走到 b 的长度为 1 的边。
输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。
数据范围
1≤n,m≤10^5
输入样例:
4 5
1 2
2 3
3 4
1 3
1 4
输出样例:
1
模板代码
#include <stdio.h>
#include <iostream>
#include <cstring>
#include <queue>
#include <unordered_map>
#include <algorithm>
using namespace std;
const int N=1e5+10,M=2*N;
int n,m;
int h[N],e[M],ne[M];
int 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;
d[1]=0;
q.push(1);
while(q.size()){
int t=q.front();
q.pop();
for(int i=h[t];~i;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);
memset(d,-1,sizeof d);
while(m--){
int a,b;
cin>>a>>b;
add(a,b);
}
cout<<bfs()<<endl;
return 0;
}
拓扑排序
848. 有向图的拓扑序列
给定一个 n 个点 m 条边的有向图,点的编号是 1 到 n,图中可能存在重边和自环。
请输出任意一个该有向图的拓扑序列,如果拓扑序列不存在,则输出 −1。
若一个由图中所有点构成的序列 A 满足:对于图中的每条边 (x,y),x 在 A 中都出现在 y 之前,则称 A 是该图的一个拓扑序列。
输入格式
第一行包含两个整数 n 和 m。
接下来 m 行,每行包含两个整数 x 和 y,表示存在一条从点 x 到点 y 的有向边 (x,y)。
输出格式
共一行,如果存在拓扑序列,则输出任意一个合法的拓扑序列即可。否则输出 −1。
数据范围
1≤n,m≤10^5
输入样例:
3 3
1 2
2 3
1 3
输出样例:
1 2 3
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
#include <unordered_map>
using namespace std;
const int N = 100010;
int n, m;
int h[N], e[N], ne[N], idx;
int d[N];
int q[N];
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
// 拓扑排序
bool topsort() {
int hh = 0, tt = -1;
for (int i = 1; i <= n; i++)
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;
}
}
return tt == n - 1;
}
int main() {
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
for (int i = 0; i < m; i++) {
int a, b;
scanf("%d%d", &a, &b);
add(a, b);
d[b]++;
}
if (!topsort()) puts("-1");
else {
for (int i = 0; i < n; i++) printf("%d ", q[i]);
puts("");
}
return 0;
}
Dijkstra
849. Dijkstra求最短路 I
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为正值。
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。
输入格式
第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。
如果路径不存在,则输出 −1。
数据范围
1≤n≤500,
1≤m≤105,
图中涉及边长均不超过10000。
输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3
解题思路
解题模板
#include <iostream>
#include <cstring>
#include <stack>
#include <unordered_map>
#include <algorithm>
#include <vector>
using namespace std;
const int N = 510;
int n, m;
int g[N][N]; // 用邻接矩阵存储两点之间的路径
int dist[N]; // dist[i]表示第1号点到第i号点的最短路径
bool st[N]; // 第i个点是否纳入了最短路中
int diskstra() {
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for (int i = 0; i < n - 1; i++) { // 循环n-1次
int t = -1;
for (int j = 1; j <= n; j++) // 遍历所有的点,寻找未纳入最短路中,且到第1个点距离最小的点t
if (!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
for (int j = 1; j <= n; j++) // 用点t更新其他点到原点的最短路
dist[j] = min(dist[j], dist[t] + g[t][j]);
st[t] = true;
}
if (dist[n] == 0x3f3f3f3f) return -1;
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); // 因为有可能出现重边,只需要保留最小的那条边
}
printf("%d\n", diskstra());
return 0;
}
850. Dijkstra求最短路 II
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为非负值。
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。
输入格式
第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。
如果路径不存在,则输出 −1。
数据范围
1≤n,m≤1.5×105,
图中涉及边长均不小于 0,且不超过 10000。
数据保证:如果最短路存在,则最短路的长度不超过 109。
输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3
模板代码
#include <iostream>
#include <cstring>
#include <stack>
#include <unordered_map>
#include <algorithm>
#include <vector>
#include <queue>
using namespace std;
typedef pair<int, int> PII;
const int N = 1e6 + 10;
int n, m;
int h[N], w[N], e[N], ne[N], idx; //用邻接表的形式存储有向图
int dist[N];
int 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; // 小顶堆(到1号点的最短距离,第j号点)
heap.push({ 0,1 }); // 初始结点压入堆中
while (heap.size()) {
auto t = heap.top(); // 堆顶元素就是到1号店距离最小的点
heap.pop();
int ver = t.second, distance = t.first;
if (st[ver]) continue;
st[ver] = true;
for (int i = h[ver]; i != -1; i = ne[i]) { // 更新所有与i号点邻接元素的dist值
int j = e[i];
if (dist[j] > dist[ver] + w[i]) {
dist[j] = dist[ver] + w[i];
heap.push({ dist[j],j }); // 更新后的值,重新压入堆中
}
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
return 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);
}
cout << dijkstra() << endl;
return 0;
}
bellman-ford
853. 有边数限制的最短路
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出从 1 号点到 n 号点的最多经过 k 条边的最短距离,如果无法从 1 号点走到 n 号点,输出 impossible。
注意:图中可能 存在负权回路 。
输入格式
第一行包含三个整数 n,m,k。
接下来 m 行,每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
点的编号为 1∼n。
输出格式
输出一个整数,表示从 1 号点到 n 号点的最多经过 k 条边的最短距离。
如果不存在满足条件的路径,则输出 impossible。
数据范围
1≤n,k≤500,
1≤m≤10000,
1≤x,y≤n,
任意边长的绝对值不超过 10000。
输入样例:
3 3 1
1 2 1
2 3 1
1 3 3
输出样例:
3
模板代码
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510, M = 10010;
struct Edge
{
int a, b, c;
}edges[M];
int n, m, k;
int dist[N];
int last[N];
void bellman_ford()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for (int i = 0; i < k; i ++ ) // 最多选择k条边
{
memcpy(last, dist, sizeof dist);
for (int j = 0; j < m; j ++ ) // 遍历全部的m条边
{
auto e = edges[j];
dist[e.b] = min(dist[e.b], last[e.a] + e.c); // 在上一次的更新结果上进行更新
}
}
}
int main()
{
scanf("%d%d%d", &n, &m, &k);
for (int i = 0; i < m; i ++ )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
edges[i] = {a, b, c};
}
bellman_ford();
if (dist[n] > 0x3f3f3f3f / 2) puts("impossible");
else printf("%d\n", dist[n]);
return 0;
}
spfa
851. spfa求最短路
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 impossible。
数据保证不存在负权回路。
输入格式
第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。
如果路径不存在,则输出 impossible。
数据范围
1≤n,m≤105,
图中涉及边长绝对值均不超过 10000。
输入样例:
3 3
1 2 5
2 3 -3
1 3 4
输出样例:
2
模板代码
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 100010;
int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[N];
bool st[N]; // st[i] 用于记录i号点是否在队列中
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()) // 类似于BFS遍历
{
int t = q.front();
q.pop();
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > dist[t] + w[i]) // 更新其他的点
{
dist[j] = dist[t] + w[i];
if (!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
return 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);
}
int t = spfa();
if (t == 0x3f3f3f3f) puts("impossible");
else printf("%d\n", t);
return 0;
}
852. spfa判断负环
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你判断图中是否存在负权回路。
输入格式
第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
输出格式
如果图中存在负权回路,则输出 Yes,否则输出 No。
数据范围
1≤n≤2000,
1≤m≤10000,
图中涉及边长绝对值均不超过 10000。
输入样例:
3 3
1 2 -1
2 3 4
3 1 -4
输出样例:
Yes
解题思路
判断负环:统计当前每个点的最短路中所包含的边数,如果某点的最短路所包含的边数大于等于n,则也说明存在负环
dist[x]: 记录虚拟源点到x的最短距离,
cnt[x] : 记录当前x点到虚拟源点最短路的边数,初始每个点到虚拟源点的距离为0,只要他能再走n步,即cnt[x] >= n,则表示该图中一定存在负环,由于从虚拟源点到x至少经过n条边时,则说明图中至少有n + 1个点,表示一定有点是重复使用
若dist[j] > dist[t] + w[i],则表示从t点走到j点能够让权值变少,因此进行对该点j进行更新,并且对应cnt[j] = cnt[t] + 1,往前走一步
模板代码
#include <cstring>
#include <iostream>
#include <algorithm>
#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];
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()
{
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();
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i]) // 遍历所有与i号点相邻的点
{
int j = e[i];
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n) return true;
if (!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
return false;
}
int main()
{
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);
}
if (spfa()) puts("Yes");
else puts("No");
return 0;
}
Floyd 求最短路
854. Floyd求最短路
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,边权可能为负数。
再给定 k 个询问,每个询问包含两个整数 x 和 y,表示查询从点 x 到点 y 的最短距离,如果路径不存在,则输出 impossible。
数据保证图中不存在负权回路。
输入格式
第一行包含三个整数 n,m,k。
接下来 m 行,每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
接下来 k 行,每行包含两个整数 x,y,表示询问点 x 到点 y 的最短距离。
输出格式
共 k 行,每行输出一个整数,表示询问的结果,若询问两点间不存在路径,则输出 impossible。
数据范围
1≤n≤200,
1≤k≤n2
1≤m≤20000,
图中涉及边长绝对值均不超过 10000。
输入样例:
3 3 2
1 2 1
2 3 2
1 3 1
2 1
1 3
输出样例:
impossible
1
模板代码
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 210, INF = 0x3f3f3f3f;
int n,m,Q;
int d[N][N];
int main() {
cin>> n>>m>>Q;
memset(d,0x3f,sizeof d);
for(int i=1;i<=n;i++) d[i][i]=0;
while(m--){
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
d[a][b]=min(d[a][b],c);
}
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]);
while(Q--){
int a,b;
cin>>a>>b;
int c=d[a][b];
if(c>INF/2) puts("impossible");
else printf("%d\n",c) ;
}
return 0;
}
最短路问题总结
Prim
858. Prim算法求最小生成树
给定一个 n 个点 m 条边的无向图,图中可能存在重边和自环,边权可能为负数。
求最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible。
给定一张边带权的无向图 G=(V,E),其中 V 表示图中点的集合,E 表示图中边的集合,n=|V|,m=|E|。
由 V 中的全部 n 个顶点和 E 中 n−1 条边构成的无向连通子图被称为 G 的一棵生成树,其中边的权值之和最小的生成树被称为无向图 G 的最小生成树。
输入格式
第一行包含两个整数 n 和 m。
接下来 m 行,每行包含三个整数 u,v,w,表示点 u 和点 v 之间存在一条权值为 w 的边。
输出格式
共一行,若存在最小生成树,则输出一个整数,表示最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible。
数据范围
1≤n≤500,
1≤m≤105,
图中涉及边的边权的绝对值均不超过 10000。
输入样例:
4 5
1 2 1
1 3 2
1 4 3
2 3 2
3 4 4
输出样例:
6
模板代码
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510, INF = 0x3f3f3f3f;
int n, m;
int g[N][N];
int dist[N];// dist[i]: i号点到生成树集合的最短距离
bool st[N];
int prim(){
memset(dist,0x3f,sizeof dist);
int res=0;
for(int i=0;i<n;i++){ // 选择n个点
int t=-1;
for(int j=1;j<=n;j++) // 寻找不在生成树集合中,dist最小的点加入集合中
if(!st[j]&&(t==-1||dist[t]>dist[j]))
t=j;
if(i&&dist[t]==INF) return INF;
st[t]=true;
if(i) res+=dist[t];
for(int j=1;j<=n;j++) // 更新其他的点
dist[j]=min(dist[j],g[t][j]);
}
return res;
}
int main() {
scanf("%d%d", &n, &m);
memset(g, 0x3f, sizeof g);
for(int i=0;i<m;i++){
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{
cout<< t<<endl;
}
return 0;
}
859. Kruskal算法求最小生成树
给定一个 n 个点 m 条边的无向图,图中可能存在重边和自环,边权可能为负数。
求最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible。
给定一张边带权的无向图 G=(V,E),其中 V 表示图中点的集合,E 表示图中边的集合,n=|V|,m=|E|。
由 V 中的全部 n 个顶点和 E 中 n−1 条边构成的无向连通子图被称为 G 的一棵生成树,其中边的权值之和最小的生成树被称为无向图 G 的最小生成树。
输入格式
第一行包含两个整数 n 和 m。
接下来 m 行,每行包含三个整数 u,v,w,表示点 u 和点 v 之间存在一条权值为 w 的边。
输出格式
共一行,若存在最小生成树,则输出一个整数,表示最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible。
数据范围
1≤n≤10^5,
1≤m≤2∗10^5,
图中涉及边的边权的绝对值均不超过 1000。
输入样例:
4 5
1 2 1
1 3 2
1 4 3
2 3 2
3 4 4
输出样例:
6
代码模板
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010, M = 200010, INF = 0x3f3f3f3f;
int n, m;
int p[N];
struct Edge
{
int a, b, w;
bool operator< (const Edge &W)const
{
return w < W.w;
}
}edges[M];
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int kruskal()
{
sort(edges, edges + m); // 按照权重从小到大排序
for (int i = 1; i <= n; i ++ ) p[i] = i; // 初始化并查集
int res = 0, cnt = 0;
for (int i = 0; i < m; i ++ ) // 遍历所有的边
{
int a = edges[i].a, 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;
return res;
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 0; i < m; i ++ )
{
int a, b, w;
scanf("%d%d%d", &a, &b, &w);
edges[i] = {a, b, w};
}
int t = kruskal();
if (t == INF) puts("impossible");
else printf("%d\n", t);
return 0;
}
860. 染色法判定二分图
给定一个 n 个点 m 条边的无向图,图中可能存在重边和自环。
请你判断这个图是否是二分图。
输入格式
第一行包含两个整数 n 和 m。
接下来 m 行,每行包含两个整数 u 和 v,表示点 u 和点 v 之间存在一条边。
输出格式
如果给定图是二分图,则输出 Yes,否则输出 No。
数据范围
1≤n,m≤10^5
输入样例:
4 4
1 3
1 4
2 3
2 4
输出样例:
Yes
模板代码
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010, M = 200010;
int n, m;
int h[N], e[M], ne[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;
for (int i = h[u]; i != -1; i = ne[i]) {
int j = e[i];
if (!color[j]) {
if (!dfs(j, 3 - c)) return false;
}
else if (color[j] == c) return false;
}
return true;
}
int main() {
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while (m--) {
int a, b;
scanf("%d%d", &a, &b);
add(a, b), add(b, a);
}
bool flag = true;
for(int i=1;i<=n;i++)
if (!color[i]) {
if (!dfs(i, 1)) {
flag = false;
break;
}
}
if (flag) puts("Yes");
else puts("No");
return 0;
}
861. 二分图的最大匹配
给定一个二分图,其中左半部包含 n1 个点(编号 1∼n1),右半部包含 n2 个点(编号 1∼n2),二分图共包含 m 条边。
数据保证任意一条边的两个端点都不可能在同一部分中。
请你求出二分图的最大匹配数。
二分图的匹配:给定一个二分图 G,在 G 的一个子图 M 中,M 的边集 {E} 中的任意两条边都不依附于同一个顶点,则称 M 是一个匹配。
二分图的最大匹配:所有匹配中包含边数最多的一组匹配被称为二分图的最大匹配,其边数即为最大匹配数。
输入格式
第一行包含三个整数 n1、 n2 和 m。
接下来 m 行,每行包含两个整数 u 和 v,表示左半部点集中的点 u 和右半部点集中的点 v 之间存在一条边。
输出格式
输出一个整数,表示二分图的最大匹配数。
数据范围
1≤n1,n2≤500,
1≤u≤n1,
1≤v≤n2,
1≤m≤105
输入样例:
2 2 4
1 1
1 2
2 1
2 2
输出样例:
2
代码模板
#include <cstring>
#include <iostream>
#include <algorithm>
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;
// 如果结点j尚未被匹配或者结点j可以有其他的结点相匹配
if (match[j] == 0 || find(match[j]))
{
match[j] = x;
return true;
}
}
}
return false;
}
int main()
{
scanf("%d%d%d", &n1, &n2, &m);
memset(h, -1, sizeof h);
while (m -- )
{
int a, b;
scanf("%d%d", &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;
}
第四讲:数学知识
质数
866. 试除法判定质数
给定 n 个正整数 ai,判定每个数是否是质数。
输入格式
第一行包含整数 n。
接下来 n 行,每行包含一个正整数 ai。
输出格式
共 n 行,其中第 i 行输出第 i 个正整数 ai 是否为质数,是则输出 Yes,否则输出 No。
数据范围
1≤n≤100,
1≤ai≤2^31−1
输入样例:
2
2
6
输出样例:
Yes
No
模板代码
#include <iostream>
#include <algorithm>
using namespace std;
bool is_prime(int x) {
if (x < 2) return false;
for (int i = 2; i <= x / i; i++)
if (x % i == 0)
return false;
return true;
}
int main() {
int n;
cin >> n;
while (n--) {
int x;
cin >> x;
if (is_prime(x)) puts("Yes");
else puts("No");
}
return 0;
}
867. 分解质因数
给定 n 个正整数 ai,将每个数分解质因数,并按照质因数从小到大的顺序输出每个质因数的底数和指数。
输入格式
第一行包含整数 n。
接下来 n 行,每行包含一个正整数 ai。
输出格式
对于每个正整数 ai,按照从小到大的顺序输出其分解质因数后,每个质因数的底数和指数,每个底数和指数占一行。
每个正整数的质因数全部输出完毕后,输出一个空行。
数据范围
1≤n≤100,
2≤ai≤2×109
输入样例:
2
6
8
输出样例:
2 1
3 1
2 3
代码模板
#include <iostream>
#include <algorithm>
using namespace std;
void divide(int x) {
for(int i=2;i<=x/i; i++)
if (x % i == 0) {
int s = 0;
while (x % i == 0) x /= i, s++;
cout << i << ' ' << s << endl;
}
if (x > 1) cout << x << ' ' << 1 << endl;
cout << endl;
}
int main() {
int n;
cin >> n;
while (n--) {
int x;
cin >> x;
divide(x);
}
return 0;
}
868. 筛质数
给定一个正整数 n,请你求出 1∼n 中质数的个数。
输入格式
共一行,包含整数 n。
输出格式
共一行,包含一个整数,表示 1∼n 中质数的个数。
数据范围
1≤n≤10^6
输入样例:
8
输出样例:
4
模板代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1000010;
int primes[N], cnt;
bool st[N];
void get_primes(int n) {
for (int i = 2; i <= n; i++) {
if (st[i]) continue;
primes[cnt++] = i;
for (int j = i + i; j <= n; j += i)
st[j] = true;
}
}
int main() {
int n;
cin >> n;
get_primes(n);
cout << cnt << endl;
return 0;
}
约数
869. 试除法求约数
给定 n 个正整数 ai,对于每个整数 ai,请你按照从小到大的顺序输出它的所有约数。
输入格式
第一行包含整数 n。
接下来 n 行,每行包含一个整数 ai。
输出格式
输出共 n 行,其中第 i 行输出第 i 个整数 ai 的所有约数。
数据范围
1≤n≤100,
2≤ai≤2×10^9
输入样例:
2
6
8
输出样例:
1 2 3 6
1 2 4 8
代码模板
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
vector <int> get_divisors(int x) {
vector<int> res;
for(int i=1;i<=x/i;i++)
if (x % i == 0) {
res.push_back(i);
if (i != x / i) res.push_back(x / i);
}
sort(res.begin(), res.end());
return res;
}
int main() {
int n;
cin >> n;
while (n--) {
int x;
cin >> x;
auto res = get_divisors(x);
for (auto x : res) cout << x << ' ';
cout << endl;
}
return 0;
}
870. 约数个数
给定 n 个正整数 ai,请你输出这些数的乘积的约数个数,答案对 10^9+7 取模。
输入格式
第一行包含整数 n。
接下来 n 行,每行包含一个整数 ai。
输出格式
输出一个整数,表示所给正整数的乘积的约数个数,答案需对 10^9+7 取模。
数据范围
1≤n≤100,
1≤ai≤2×10^9
输入样例:
3
2
6
8
输出样例:
12
模板代码
#include <iostream>
#include <algorithm>
#include <vector>
#include <unordered_map>
using namespace std;
typedef long long LL;
const int N = 110, mod = 1e9 + 7;
int main() {
int n;
cin >> n;
unordered_map<int, int> primes;
while (n--) {
int x;
cin >> x;
for(int i=2;i<=x/i;i++)
while (x % i == 0) {
x /= i;
primes[i]++;
}
if (x > 1) primes[x]++;
}
LL res = 1;
for (auto p : primes) res = res * (p.second + 1) % mod;
cout << res << endl;
return 0;
}
871. 约数之和
给定 n 个正整数 ai,请你输出这些数的乘积的约数之和,答案对 109+7 取模。
输入格式
第一行包含整数 n。
接下来 n 行,每行包含一个整数 ai。
输出格式
输出一个整数,表示所给正整数的乘积的约数之和,答案需对 109+7 取模。
数据范围
1≤n≤100,
1≤ai≤2×109
输入样例:
3
2
6
8
输出样例:
252
解题思路
代码模板
#include <iostream>
#include <algorithm>
#include <vector>
#include <unordered_map>
using namespace std;
typedef long long LL;
const int N = 110, mod = 1e9 + 7;
int main() {
int n;
cin >> n;
unordered_map<int, int> primes;
while (n--) {
int x;
cin >> x;
for(int i=2;i<=x/i;i++)
while (x % i == 0) {
x /= i;
primes[i]++;
}
if (x > 1) primes[x]++;
}
LL res = 1;
for (auto p : primes) {
LL a = p.first, b = p.second;
LL t = 1;
while (b--) t = (t * a + 1) % mod;
res = res * t % mod;
}
cout << res << endl;
}
872. 最大公约数
给定 n 对正整数 ai,bi,请你求出每对数的最大公约数。
输入格式
第一行包含整数 n。
接下来 n 行,每行包含一个整数对 ai,bi。
输出格式
输出共 n 行,每行输出一个整数对的最大公约数。
数据范围
1≤n≤105,
1≤ai,bi≤2×109
输入样例:
2
3 6
4 6
输出样例:
3
2
模板代码
#include <iostream>
#include <algorithm>
using namespace std;
int gcd(int a, int b) {
return b ? gcd(b, a % b) : a;
}
int main() {
int n;
cin >> n;
while (n--) {
int a, b;
scanf("%d%d", &a, &b);
printf("%d\n", gcd(a, b));
}
return 0;
}
873. 欧拉函数
给定 n 个正整数 ai,请你求出每个数的欧拉函数。
欧拉函数的定义
输入格式
第一行包含整数 n。
接下来 n 行,每行包含一个正整数 ai。
输出格式
输出共 n 行,每行输出一个正整数 ai 的欧拉函数。
数据范围
1≤n≤100,
1≤ai≤2×109
输入样例:
3
3
6
8
输出样例:
2
2
4
模板代码
#include <iostream>
using namespace std;
int phi(int x)
{
int res = x;
for (int i = 2; i <= x / i; i ++ )
if (x % i == 0)
{
res = res / i * (i - 1);
while (x % i == 0) x /= i;
}
if (x > 1) res = res / x * (x - 1);
return res;
}
int main()
{
int n;
cin >> n;
while (n -- )
{
int x;
cin >> x;
cout << phi(x) << endl;
}
return 0;
}
874. 筛法求欧拉函数
给定一个正整数 n,求 1∼n 中每个数的欧拉函数之和。
输入格式
共一行,包含一个整数 n。
输出格式
共一行,包含一个整数,表示 1∼n 中每个数的欧拉函数之和。
数据范围
1≤n≤106
输入样例:
6
输出样例:
12
模板代码
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 1000010;
int primes[N], cnt;
int euler[N];
bool st[N];
void get_eulers(int n)
{
euler[1] = 1;
for (int i = 2; i <= n; i ++ )
{
if (!st[i])
{
primes[cnt ++ ] = i;
euler[i] = i - 1;
}
for (int j = 0; primes[j] <= n / i; j ++ )
{
int t = primes[j] * i;
st[t] = true;
if (i % primes[j] == 0)
{
euler[t] = euler[i] * primes[j];
break;
}
euler[t] = euler[i] * (primes[j] - 1);
}
}
}
int main()
{
int n;
cin >> n;
get_eulers(n);
LL res = 0;
for (int i = 1; i <= n; i ++ ) res += euler[i];
cout << res << endl;
return 0;
}
快速幂
875. 快速幂
给定 n 组 ai,bi,pi,对于每组数据,求出 $a^{b_i}mod p_i$的值。
输入格式
第一行包含整数 n。
接下来 n 行,每行包含三个整数 ai,bi,pi。
输出格式
对于每组数据,输出一个结果,表示 abiimodpi 的值。
每个结果占一行。
数据范围
1≤n≤100000,
1≤ai,bi,pi≤2×109
输入样例:
2
3 2 5
4 3 9
输出样例:
4
1
解题思路
预处理如下
l
o
g
k
log_k
logk 个数,用其中若干个数组合成
a
k
m
o
d
a^km od
akmod
p
p
p
a
2
0
=
a
1
a^{2^0}=a^1
a20=a1
a
2
1
=
(
a
2
0
)
2
a^{2^1}=(a^{2^0})^2
a21=(a20)2
…
a
2
l
o
g
k
=
(
a
2
l
o
g
k
−
1
)
2
a^{2^{log_k}}=(a^{2^{log_k-1}})^2
a2logk=(a2logk−1)2
规律:每个数都是前一个数的平方
求
a
k
m
o
d
a^k mod
akmod
p
p
p 其中
(
k
)
10
(k)_{10}
(k)10 可以用二进制数
(
x
1
x
2
x
3
.
.
.
x
n
)
2
(x_1x_2x_3...x_n)_2
(x1x2x3...xn)2 表示
a
k
=
a
2
x
1
+
2
x
2
.
.
+
2
x
n
=
a
2
x
1
∗
a
2
x
2
.
.
∗
a
2
x
n
a^k=a^{2^{x_1}+2^{x_2}..+2^{x_n}}=a^{2^{x_1}}* a^{2^{x_2}}..*a^{2^{x_n}}
ak=a2x1+2x2..+2xn=a2x1∗a2x2..∗a2xn
代码模板
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
LL qmi(int a, int b, int p)
{
LL res = 1 % p;
while (b)
{
if (b & 1) res = res * a % p;
a = a * (LL)a % p;
b >>= 1;
}
return res;
}
int main()
{
int n;
scanf("%d", &n);
while (n -- )
{
int a, b, p;
scanf("%d%d%d", &a, &b, &p);
printf("%lld\n", qmi(a, b, p));
}
return 0;
}
高斯消元
高斯消元解线性方程组
输入一个包含 n 个方程 n 个未知数的线性方程组。
方程组中的系数为实数。
求解这个方程组。
下图为一个包含 m 个方程 n 个未知数的线性方程组示例:
输入格式
第一行包含整数 n。
接下来 n 行,每行包含 n+1 个实数,表示一个方程的 n 个系数以及等号右侧的常数。
输出格式
如果给定线性方程组存在唯一解,则输出共 n 行,其中第 i 行输出第 i 个未知数的解,结果保留两位小数。
如果给定线性方程组存在无数解,则输出 Infinite group solutions。
如果给定线性方程组无解,则输出 No solution。
数据范围
1≤n≤100,
所有输入系数以及常数均保留两位小数,绝对值均不超过 100。
输入样例:
3
1.00 2.00 -1.00 -6.00
2.00 1.00 -3.00 -9.00
-1.00 -1.00 2.00 7.00
输出样例:
1.00
-2.00
3.00
解题思路
1)枚举每一列,找到绝对值最大的一行
2)将该行换到最上面
3)将该行的第一个数变成1
4)将下面所有行的第c列清成0
模板代码
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;
const int N=110;
const double eps=1e-8;
int n;
double a[N][N];
// 高斯消元,答案存放在a[i][n]中
int gauss(){
int c,r;
for(c=0,r=0;c<n;c++){ // 先遍历所有的列
int t=r;
for(int i=r;i<n;i++) // 寻找该列上绝对值最大的元素所在的行
if(fabs(a[i][c])>fabs(a[t][c]))
t=i;
if(fabs(a[t][c])<eps) continue;
for(int i=c;i<=n;i++) swap(a[t][i],a[r][i]); // 将绝对值最大的行换到最顶端
for(int i=n;i>=c;i--) a[r][i] /= a[r][c]; // 将当前行的首位变成1
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++;
}
if(r<n){
for(int i=r;i<n;i++)
if(fabs(a[i][n])>eps)
return 2; // 无解
return 1; // 无穷多组解
}
for(int i=n-1;i>=0;i--)
for(int j=i+1;j<n;j++)
a[i][n]-=a[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]);
int t=gauss();
if(t==2) puts("No solution");
else if(t==1) puts("Infinite group solutions");
else{
for(int i=0;i<n;i++){
if(fabs(a[i][n])<eps) a[i][n]=0;
printf("%.2lf\n",a[i][n]);
}
}
return 0;
}
求组合数
885. 求组合数 I
给定 n 组询问,每组询问给定两个整数 a,b,请你输出 C^b_a mod(10^9+7) 的值。
输入格式
第一行包含整数 n。
接下来 n 行,每行包含一组 a 和 b。
输出格式
共 n 行,每行输出一个询问的解。
数据范围
1≤n≤10000,
1≤b≤a≤2000
输入样例:
3
3 1
5 3
2 2
输出样例:
3
10
1
解题思路
C
a
b
=
C
a
−
1
b
+
C
a
−
1
b
−
1
C_a^b=C_{a-1}^b+C_{a-1}^{b-1}
Cab=Ca−1b+Ca−1b−1
模板代码
#include <iostream>
using namespace std;
const int N=2010,mod=1e9+7;
int c[N][N];
void init(){
for(int i=0;i<N;i++)
for(int j=0;j<=i;j++)
if(!j) c[i][j]=1;
else c[i][j]=(c[i-1][j]+c[i-1][j-1])%mod;
}
int main(){
int n;
init();
scanf("%d",&n);
while(n--){
int a,b;
scanf("%d%d",&a,&b);
printf("%d\n",c[a][b]);
}
return 0;
}
容斥原理
890. 能被整除的数
给定一个整数 n 和 m 个不同的质数 p1,p2,…,pm。
请你求出 1∼n 中能被 p1,p2,…,pm 中的至少一个数整除的整数有多少个。
输入格式
第一行包含整数 n 和 m。
第二行包含 m 个质数。
输出格式
输出一个整数,表示满足条件的整数的个数。
数据范围
1≤m≤16,
1≤n,pi≤10^9
输入样例:
10 2
2 3
输出样例:
7
模板代码
#include <algorithm>
#include <iostream>
using namespace std;
typedef long long LL;
const int N =20;
int p[N],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++){
int t=1; // 选中的集合对应的乘积
int s=0; // 选中的集合数量
// 枚举当前状态对应的每一位
for(int j=0;j<m;j++){
if(i>>j&1){ // 如果当前集合被选中
if((LL)t*p[j]>n){ // 如果乘积大于n ,则n/t=0 跳出循环
t=-1;
break;
}
s++;
t*=p[j];
}
}
if(t==-1) continue;
// 根据容斥原理,
if(s&1) res+=n/t; // 如果选中的集合个数为奇数个,则加上该集合中的状态数量
else res-=n/t; // 否则减去该集合中的状态数量
}
cout<< res<<endl;
return 0;
}
博弈论
891. Nim游戏
给定 n 堆石子,两位玩家轮流操作,每次操作可以从任意一堆石子中拿走任意数量的石子(可以拿完,但不能不拿),最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
输入格式
第一行包含整数 n。
第二行包含 n 个数字,其中第 i 个数字表示第 i 堆石子的数量。
输出格式
如果先手方必胜,则输出 Yes。
否则,输出 No。
数据范围
1≤n≤10^5,
1≤每堆石子数≤10^9
输入样例:
2
2 3
输出样例:
Yes
解题思路
必胜状态:先手进行某一个操作,留给后手是一个必败状态时,对于先手来说是一个必胜状态。即先手可以走到某一个必败状态。
必败状态:先手无论如何操作,留给后手都是一个必胜状态时,对于先手来说是一个必败状态。即先手走不到任何一个必败状态。
假设n堆石子,石子数目分别是
a
1
,
a
2
,
…
,
a
n
a1,a2,…,an
a1,a2,…,an如果
a
1
⊕
a
2
⊕
…
⊕
a
n
≠
0
a1⊕a2⊕…⊕an≠0
a1⊕a2⊕…⊕an=0,先手必胜;否则先手必败。
证明:
模板代码
#include <iostream>
#include <cstdio>
using namespace std;
/*
先手必胜状态:先手操作完,可以走到某一个必败状态
先手必败状态:先手操作完,走不到任何一个必败状态
先手必败状态:a1 ^ a2 ^ a3 ^ ... ^an = 0
先手必胜状态:a1 ^ a2 ^ a3 ^ ... ^an ≠ 0
*/
int main(){
int n;
scanf("%d", &n);
int res = 0;
for(int i = 0; i < n; i++) {
int x;
scanf("%d", &x);
res ^= x;
}
if(res == 0) puts("No");
else puts("Yes");
}
892. 台阶-Nim游戏
现在,有一个 n 级台阶的楼梯,每级台阶上都有若干个石子,其中第 i 级台阶上有 ai 个石子(i≥1)。
两位玩家轮流操作,每次操作可以从任意一级台阶上拿若干个石子放到下一级台阶中(不能不拿)。
已经拿到地面上的石子不能再拿,最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
输入格式
第一行包含整数 n。
第二行包含 n 个整数,其中第 i 个整数表示第 i 级台阶上的石子数 ai。
输出格式
如果先手方必胜,则输出 Yes。
否则,输出 No。
数据范围
1≤n≤10^5,
1≤ai≤10^9
输入样例:
3
2 1 3
输出样例:
Yes
解题思路
此时我们需要将奇数台阶看做一个经典的Nim游戏,如果先手时奇数台阶上的值的异或值为0,则先手必败,反之必胜
证明:
先手时,如果奇数台阶异或非0,根据经典Nim游戏,先手总有一种方式使奇数台阶异或为0,于是先手留了奇数台阶异或为0的状态给后手
于是轮到后手:
①当后手移动偶数台阶上的石子时,先手只需将对手移动的石子继续移到下一个台阶,这样奇数台阶的石子相当于没变,于是留给后手的又是奇数台阶异或为0的状态
②当后手移动奇数台阶上的石子时,留给先手的奇数台阶异或非0,根据经典Nim游戏,先手总能找出一种方案使奇数台阶异或为0
因此无论后手如何移动,先手总能通过操作把奇数异或为0的情况留给后手,当奇数台阶全为0时,只留下偶数台阶上有石子。
(核心就是:先手总是把奇数台阶异或为0的状态留给对面,即总是将必败态交给对面)
因为偶数台阶上的石子要想移动到地面,必然需要经过偶数次移动,又因为奇数台阶全0的情况是留给后手的,因此先手总是可以将石子移动到地面,当将最后一个(堆)石子移动到地面时,后手无法操作,即后手失败。
因此如果先手时奇数台阶上的值的异或值为非0,则先手必胜,反之必败!
模板代码
#include <iostream>
using namespace std;
int main()
{
int res = 0;
int n;
cin >> n;
for(int i = 1 ; i <= n ; i++)
{
int x;
cin >> x;
if(i % 2) res ^= x;
}
if(res) puts("Yes");
else puts("No");
return 0;
}
第5讲:动态规划
背包问题
2. 01背包问题
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
8
模板代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i];
for (int i = 1; i <= n; i ++ )
for (int j = m; j >= v[i]; j -- )
f[j] = max(f[j], f[j - v[i]] + w[i]);
cout << f[m] << endl;
return 0;
}
3. 完全背包问题
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。
第 i 种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 种物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
10
模板代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> v[i] >> w[i];
for (int i = 1; i <= n; i++)
for (int j = v[i]; j <= m; j++)
f[j] = max(f[j], f[j - v[i]] + w[i]);
cout << f[m] << endl;
}
4. 多重背包问题 I
有 N 种物品和一个容量是 V 的背包。
第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤100
0<vi,wi,si≤100
输入样例
4 5
1 2 3
2 4 1
3 4 3
4 5 2
输出样例:
10
模板代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110;
int n, m;
int v[N], w[N], s[N];
int f[N][N];
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i] >> s[i];
for (int i = 1; i <= n; i ++ )
for (int j = 0; j <= m; j ++ )
for (int k = 0; k <= s[i] && k * v[i] <= j; k ++ )
f[i][j] = max(f[i][j], f[i - 1][j - v[i] * k] + w[i] * k);
cout << f[n][m] << endl;
return 0;
}
5. 多重背包问题 II
有 N 种物品和一个容量是 V 的背包。
第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N≤1000
0<V≤2000
0<vi,wi,si≤2000
提示:
本题考查多重背包的二进制优化方法。
输入样例
4 5
1 2 3
2 4 1
3 4 3
4 5 2
输出样例:
10
模板代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 12010, M = 2010;
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;
while (k <= s)
{
cnt ++ ;
v[cnt] = a * k;
w[cnt] = b * k;
s -= k;
k *= 2;
}
if (s > 0)
{
cnt ++ ;
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] << endl;
return 0;
}
9. 分组背包问题
有 N 组物品和一个容量是 V 的背包。
每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行有两个整数 N,V,用空格隔开,分别表示物品组数和背包容量。
接下来有 N 组数据:
每组数据第一行有一个整数 Si,表示第 i 个物品组的物品数量;
每组数据接下来有 Si 行,每行有两个整数 vij,wij,用空格隔开,分别表示第 i 个物品组的第 j 个物品的体积和价值;
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤100
0<Si≤100
0<vij,wij≤100
输入样例
3 5
2
1 2
2 4
1
3 4
1
4 5
输出样例:
8
模板代码
#include <iostream>
#include <algorithm>
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 (v[i][k] <= j)
f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
cout << f[m] << endl;
return 0;
}
线性DP
898. 数字三角形
给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输入格式
第一行包含整数 n,表示数字三角形的层数。
接下来 n 行,每行包含若干整数,其中第 i 行表示数字三角形第 i 层包含的整数。
输出格式
输出一个整数,表示最大的路径数字和。
数据范围
1≤n≤500,
−10000≤三角形中的整数≤10000
输入样例:
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输出样例:
30
模板代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510;
int n;
int w[N][N], f[N][N];
int main() {
cin >> n;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= i; j++)
cin >> w[i][j];
for (int i = 1; i <= n; i++) f[n][i] = w[n][i];
for (int i = n - 1; i; i--)
for (int j = 1; j <= i; j++)
f[i][j] = max(f[i + 1][j], f[i + 1][j + 1]) + w[i][j];
cout << f[1][1] << endl;
}
895. 最长上升子序列
给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数 N。
第二行包含 N 个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
数据范围
1≤N≤1000,
−10^9≤数列中的数≤10^9
输入样例:
7
3 1 2 1 8 5 6
输出样例:
4
模板代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n;
int a[N], f[N];
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;
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]);
printf("%d\n", res);
}
896. 最长上升子序列 II
给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数 N。
第二行包含 N 个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
数据范围
1≤N≤100000,
−10^9≤数列中的数≤10^9
输入样例:
7
3 1 2 1 8 5 6
输出样例:
4
模板代码
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
int main(void) {
int n; cin >> n;
vector<int>arr(n);
for (int i = 0; i < n; ++i)cin >> arr[i];
vector<int>stk;//模拟堆栈
stk.push_back(arr[0]);
for (int i = 1; i < n; ++i) {
if (arr[i] > stk.back())//如果该元素大于栈顶元素,将该元素入栈
stk.push_back(arr[i]);
else//替换掉第一个大于或者等于这个数字的那个数
*lower_bound(stk.begin(), stk.end(), arr[i]) = arr[i];
}
cout << stk.size() << endl;
return 0;
}
/*
例 n: 7
arr : 3 1 2 1 8 5 6
stk : 3
1 比 3 小
stk : 1
2 比 1 大
stk : 1 2
1 比 2 小
stk : 1 2
8 比 2 大
stk : 1 2 8
5 比 8 小
stk : 1 2 5
6 比 5 大
stk : 1 2 5 6
stk 的长度就是最长递增子序列的长度
*/
897. 最长公共子序列
给定两个长度分别为 N 和 M 的字符串 A 和 B,求既是 A 的子序列又是 B 的子序列的字符串长度最长是多少。
输入格式
第一行包含两个整数 N 和 M。
第二行包含一个长度为 N 的字符串,表示字符串 A。
第三行包含一个长度为 M 的字符串,表示字符串 B。
字符串均由小写字母构成。
输出格式
输出一个整数,表示最大长度。
数据范围
1≤N,M≤1000
输入样例:
4 5
acbd
abedc
输出样例:
3
模板代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N];
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]);
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;
}
902. 最短编辑距离
给定两个字符串 A 和 B,现在要将 A 经过若干操作变为 B,可进行的操作有:
删除–将字符串 A 中的某个字符删除。
插入–在字符串 A 的某个位置插入某个字符。
替换–将字符串 A 中的某个字符替换为另一个字符。
现在请你求出,将 A 变为 B 至少需要进行多少次操作。
输入格式
第一行包含整数 n,表示字符串 A 的长度。
第二行包含一个长度为 n 的字符串 A。
第三行包含整数 m,表示字符串 B 的长度。
第四行包含一个长度为 m 的字符串 B。
字符串中均只包含大小写字母。
输出格式
输出一个整数,表示最少操作次数。
数据范围
1≤n,m≤1000
输入样例:
10
AGTCTGACGC
11
AGTAAGTAGGC
输出样例:
4
解题思路
将 字符串a[1~i] 变成 b[1~j] 的操作次数最小
1)删除操作:把a[i]删掉之后a[i]后和b[1~j]匹配
所以之前要先做到a[1~(i-1)]和b[1~j]匹配
f[i-1][j] + 1
2)插入操作:插入a[i+1]之后与b[0~j]完全匹配,所以插入的就是b[j]
那填之前a[1~i]和b[1~(j-1)]匹配
f[i][j-1] + 1
3)替换操作:把a[i]改成b[j]之后想要a[1~i]与b[1~j]匹配
那么修改这一位之前,a[1~(i-1)]应该与b[1~(j-1)]匹配
f[i-1][j-1] + 1
但是如果本来a[i]与b[j]这一位上就相等,那么不用改,即
f[i-1][j-1] + 0
模板代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N];
int main() {
scanf("%d%s", &n, a + 1);
scanf("%d%s", &m, b + 1);
for (int i = 0; i <= m; i++) f[0][i] = i;
for (int i = 0; i <= n; i++) f[i][0] = i;
for(int i=1;i<=n;i++)
for (int j = 1; j <= m; j++) {
f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);
if (a[i] == b[j]) f[i][j] = min(f[i][j], f[i - 1][j - 1]);
else f[i][j] = min(f[i][j], f[i - 1][j - 1] + 1);
}
printf("%d\n", f[n][m]);
return 0;
}
899. 编辑距离
给定 n 个长度不超过 10 的字符串以及 m 次询问,每次询问给出一个字符串和一个操作次数上限。
对于每次询问,请你求出给定的 n 个字符串中有多少个字符串可以在上限操作次数内经过操作变成询问给出的字符串。
每个对字符串进行的单个字符的插入、删除或替换算作一次操作。
输入格式
第一行包含两个整数 n 和 m。
接下来 n 行,每行包含一个字符串,表示给定的字符串。
再接下来 m 行,每行包含一个字符串和一个整数,表示一次询问。
字符串中只包含小写字母,且长度均不超过 10。
输出格式
输出共 m 行,每行输出一个整数作为结果,表示一次询问中满足条件的字符串个数。
数据范围
1≤n,m≤1000,
输入样例:
3 2
abc
acd
bcd
ab 1
acbd 2
输出样例:
1
3
模板代码
#include <iostream>
#include <algorithm>
#include <string.h>
using namespace std;
const int N = 15, M = 1010;
int n, m;
int f[N][N];
char str[M][N];
int edit_distance(char a[], char b[]) {
int la = strlen(a + 1), lb = strlen(b + 1);
for (int i = 0; i <= lb; i++) f[0][i] = i;
for (int i = 0; i <= la; i++)f[i][0] = i;
for(int i=1;i<=la;i++)
for (int j = 1; j <= lb; j++)
{
f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);
f[i][j] = min(f[i][j], f[i - 1][j - 1] + (a[i] != b[j]));
}
return f[la][lb];
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 0; i < n; i++) scanf("%s", str[i] + 1);
while (m--) {
char s[N];
int limit;
scanf("%s%d", s + 1, &limit);
int res = 0;
for (int i = 0; i < n; i++)
if (edit_distance(str[i], s) <= limit)
res++;
printf("%d\n", res);
}
return 0;
}
282. 石子合并
设有 N 堆石子排成一排,其编号为 1,2,3,…,N。
每堆石子有一定的质量,可以用一个整数来描述,现在要将这 N 堆石子合并成为一堆。
每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。
例如有 4 堆石子分别为 1 3 5 2, 我们可以先合并 1、2 堆,代价为 4,得到 4 5 2, 又合并 1,2 堆,代价为 9,得到 9 2 ,再合并得到 11,总代价为 4+9+11=24;
如果第二步是先合并 2,3 堆,则代价为 7,得到 4 7,最后一次合并代价为 11,总代价为 4+7+11=22。
问题是:找出一种合理的方法,使总的代价最小,输出最小代价。
输入格式
第一行一个数 N 表示石子的堆数 N。
第二行 N 个数,表示每堆石子的质量(均不超过 1000)。
输出格式
输出一个整数,表示最小代价。
数据范围
1≤N≤300
输入样例:
4
1 3 5 2
输出样例:
22
解题思路
关键点:最后一次合并一定是左边连续的一部分和右边连续的一部分进行合并
状态表示:
f
[
i
]
[
j
]
f[i][j]
f[i][j]表示将 i到 j 这一段石子合并成一堆的方案的集合,属性 Min
状态计算:
问题答案:
f
[
1
]
[
n
]
f[1][n]
f[1][n]
解题模板
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 310;
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 i = 1; i + len - 1 <= n; i ++ )
{
int l = i, r = i + len - 1;
f[l][r] = 1e8;
for (int k = l; k < r; k ++ )
f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
}
printf("%d\n", f[1][n]);
return 0;
}
900. 整数划分
一个正整数 n 可以表示成若干个正整数之和,形如:n=n1+n2+…+nk,其中 n1≥n2≥…≥nk,k≥1。
我们将这样的一种表示称为正整数 n 的一种划分。
现在给定一个正整数 n,请你求出 n 共有多少种不同的划分方法。
输入格式
共一行,包含一个整数 n。
输出格式
共一行,包含一个整数,表示总划分数量。
由于答案可能很大,输出结果请对 10^9+7 取模。
数据范围
1≤n≤1000
输入样例:
5
输出样例:
7
解题思路
状态表示:
f[i][j]
表示只从1~i中选,且总和等于j的方案数
状态转移方程:
由下面两式
f[i][j]=f[i-1][j]+f[i-1][j-i]+f[i-1][j-i*2]+...+f[i-1][j-i*s]
f[i][j-i]= f[i-1][j-i]+f[i-1][j-i*2]+...+f[i-1][j-i*s]
可以得到
f[i][j]=f[i-1][j]+f[i][j-i]
模板代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010, mod = 1e9 + 7;
int n;
int f[N];
int main() {
cin >> n;
f[0] = 1;
for (int i = 1; i <= n; i++)
for (int j = i; j <= n; j++)
f[j] = (f[j] + f[j - i]) % mod;
cout << f[n] << endl;
return 0;
}
338. 计数问题
给定两个整数 a 和 b,求 a 和 b 之间的所有数字中 0∼9 的出现次数。
例如,a=1024,b=1032,则 a 和 b 之间共有 9 个数如下:
1024 1025 1026 1027 1028 1029 1030 1031 1032
其中 0 出现 10 次,1 出现 10 次,2 出现 7 次,3 出现 3 次等等…
输入格式
输入包含多组测试数据。
每组测试数据占一行,包含两个整数 a 和 b。
当读入一行为 0 0 时,表示输入终止,且该行不作处理。
输出格式
每组数据输出一个结果,每个结果占一行。
每个结果包含十个用空格隔开的数字,第一个数字表示 0 出现的次数,第二个数字表示 1 出现的次数,以此类推。
数据范围
0<a,b<100000000
输入样例:
1 10
44 497
346 542
1199 1748
1496 1403
1004 503
1714 190
1317 854
1976 494
1001 1960
0 0
输出样例:
1 2 1 1 1 1 1 1 1 1
85 185 185 185 190 96 96 96 95 93
40 40 40 93 136 82 40 40 40 40
115 666 215 215 214 205 205 154 105 106
16 113 19 20 114 20 20 19 19 16
107 105 100 101 101 197 200 200 200 200
413 1133 503 503 503 502 502 417 402 412
196 512 186 104 87 93 97 97 142 196
398 1375 398 398 405 499 499 495 488 471
294 1256 296 296 296 296 287 286 286 247
解题思路
代码模板
状态压缩DP
291. 蒙德里安的梦想
求把 N×M 的棋盘分割成若干个 1×2 的长方形,有多少种方案。
例如当 N=2,M=4 时,共有 5 种方案。当 N=2,M=3 时,共有 3 种方案。
如下图所示:
输入格式
输入包含多组测试用例。
每组测试用例占一行,包含两个整数 N 和 M。
当输入用例 N=0,M=0 时,表示输入终止,且该用例无需处理。
输出格式
每个测试用例输出一个结果,每个结果占一行。
数据范围
1≤N,M≤11
输入样例:
1 2
1 3
1 4
2 2
2 3
2 4
2 11
4 11
0 0
输出样例:
1
0
1
2
3
5
144
51205
解题思路
核心:先放横着的,再放竖着的。
总方案数:等于只放横着的小方块的合法方案数
如何判断,当前方案是否合法?所有剩余的位置,能否填充满竖着的小方块。可以按列来看,每一列内部所有连续的空着的小方块,需要是偶数个。
动态规划:
状态表示:f[i,j]表示已经将前i-1列摆好,且从第i-1列,伸到第i列的所有方案。
模板代码
#include <bits/stdc++.h>
using namespace std;
const int N = 12, M = 1<< N;
long long f[N][M] ;// 第一维表示列, 第二维表示所有可能的状态
bool st[M]; //存储每种状态是否有奇数个连续的0,如果奇数个0是无效状态,如果是偶数个零置为true。
//vector<int > state[M]; //二维数组记录合法的状态
vector<vector<int>> state(M); //两种写法等价:二维数组
int m, n;
int main() {
while (cin >> n >> m, n || m) { //读入n和m,并且不是两个0即合法输入就继续读入
//第一部分:预处理1
//对于每种状态,先预处理每列不能有奇数个连续的0
for(int i = 0; i < (1 << n); i ++) {
int cnt = 0 ;//记录连续的0的个数
bool isValid = true; // 某种状态没有奇数个连续的0则标记为true
for(int j = 0; j < n; j ++) { //遍历这一列,从上到下
if ( (i >> j) & 1) {
//i >> j位运算,表示i(i在此处是一种状态)的二进制数的第j位;
// &1为判断该位是否为1,如果为1进入if
if (cnt & 1) {
//这一位为1,看前面连续的0的个数,如果是奇数(cnt &1为真)则该状态不合法
isValid =false; break;
}
cnt = 0; // 既然该位是1,并且前面不是奇数个0(经过上面的if判断),计数器清零。
//其实清不清零没有影响
}
else cnt ++; //否则的话该位还是0,则统计连续0的计数器++。
}
if (cnt & 1) isValid = false; //最下面的那一段判断一下连续的0的个数
st[i] = isValid; //状态i是否有奇数个连续的0的情况,输入到数组st中
}
//第二部分:预处理2
// 经过上面每种状态 连续0的判断,已经筛掉一些状态。
//下面来看进一步的判断:看第i-2列伸出来的和第i-1列伸出去的是否冲突
for (int j = 0; j < (1 << n); j ++) { //对于第i列的所有状态
state[j].clear(); //清空上次操作遗留的状态,防止影响本次状态。
for (int k = 0; k < (1 << n); k ++) { //对于第i-1列所有状态
if ((j & k ) == 0 && st[ j | k])
// 第i-2列伸出来的 和第i-1列伸出来的不冲突(不在同一行)
//解释一下st[j | k]
//已经知道st[]数组表示的是这一列没有连续奇数个0的情况,
//我们要考虑的是第i-1列(第i-1列是这里的主体)中从第i-2列横插过来的,
//还要考虑自己这一列(i-1列)横插到第i列的
//比如 第i-2列插过来的是k=10101,第i-1列插出去到第i列的是 j =01000,
//那么合在第i-1列,到底有多少个1呢?
//自然想到的就是这两个操作共同的结果:两个状态或。 j | k = 01000 | 10101 = 11101
//这个 j|k 就是当前 第i-1列的到底有几个1,即哪几行是横着放格子的
state[j].push_back(k);
//二维数组state[j]表示第j行,
//j表示 第i列“真正”可行的状态,
//如果第i-1列的状态k和j不冲突则压入state数组中的第j行。
//“真正”可行是指:既没有前后两列伸进伸出的冲突;又没有连续奇数个0。
}
}
//第三部分:dp开始
memset(f, 0, sizeof f);
//全部初始化为0,因为是连续读入,这里是一个清空操作。
//类似上面的state[j].clear()
f[0][0] = 1 ;// 这里需要回忆状态表示的定义
//按定义这里是:前第-1列都摆好,且从-1列到第0列伸出来的状态为0的方案数。
//首先,这里没有-1列,最少也是0列。
//其次,没有伸出来,即没有横着摆的。即这里第0列只有竖着摆这1种状态。
for (int i = 1; i <= m; i ++) { //遍历每一列:第i列合法范围是(0~m-1列)
for (int j = 0; j < (1<<n); j ++) { //遍历当前列(第i列)所有状态j
for (auto k : state[j]) // 遍历第i-1列的状态k,如果“真正”可行,就转移
f[i][j] += f[i-1][k]; // 当前列的方案数就等于之前的第i-1列所有状态k的累加。
}
}
//最后答案是什么呢?
//f[m][0]表示 前m-1列都处理完,并且第m-1列没有伸出来的所有方案数。
//即整个棋盘处理完的方案数
cout << f[m][0] << endl;
}
}
作者:ShizhengLee
链接:https://www.acwing.com/solution/content/28088/
树形DP
285. 没有上司的舞会
Ural 大学有 N 名职员,编号为 1∼N。
他们的关系就像一棵以校长为根的树,父节点就是子节点的直接上司。
每个职员有一个快乐指数,用整数 Hi 给出,其中 1≤i≤N。
现在要召开一场周年庆宴会,不过,没有职员愿意和直接上司一起参会。
在满足这个条件的前提下,主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求这个最大值。
输入格式
第一行一个整数 N。
接下来 N 行,第 i 行表示 i 号职员的快乐指数 Hi。
接下来 N−1 行,每行输入一对整数 L,K,表示 K 是 L 的直接上司。
输出格式
输出最大的快乐指数。
数据范围
1≤N≤6000,
−128≤Hi≤127
输入样例:
7
1
1
1
1
1
1
1
1 3
2 3
6 4
7 4
4 5
3 5
输出样例:
5
解题思路
f
[
u
]
[
0
]
f[u][0]
f[u][0]: 所有从以u为根的子树中选择,并且不选u这个点的方案。
f
[
u
]
[
1
]
f[u][1]
f[u][1]: 所有以u为根的子树中选,并且选择u这个点的方案。
模板代码
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N=6010;
int n;
int h[N],e[N],ne[N],idx;
int happy[N];
int f[N][2];
bool has_fa[N];
void add(int a,int b){
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void dfs(int u){
f[u][1]=happy[u]; // 若选择节点u,则加上该节点的快乐值
for(int i=h[u];i!=-1;i=ne[i]){ // 遍历结点u的所有子节点
int j=e[i];
dfs(j);
f[u][1]+=f[j][0]; // 如果选择了结点u,那么其子节点不能选择
f[u][0]+=max(f[j][0],f[j][1]); // 如果未选择结点u,那么其子节点可以选择也可以不选择
}
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&happy[i]);
memset(h,-1,sizeof h);
for(int i=0;i<n-1;i++){
int a,b;
scanf("%d%d",&a,&b); // b是a的直接上司
add(b,a);
has_fa[a]=true; // a有父节点
}
int root =1;
while(has_fa[root]) // 寻找根节点
root++;
dfs(root);
printf("%d\n",max(f[root][0],f[root][1])); // 根节点选或者不选
return 0;
}
91. 最短Hamilton路径
给定一张 n 个点的带权无向图,点从 0∼n−1 标号,求起点 0 到终点 n−1 的最短 Hamilton 路径。
Hamilton 路径的定义是从 0 到 n−1 不重不漏地经过每个点恰好一次。
输入格式
第一行输入整数 n。
接下来 n 行每行 n 个整数,其中第 i 行第 j 个整数表示点 i 到 j 的距离(记为 a[i,j])。
对于任意的 x,y,z,数据保证 a[x,x]=0,a[x,y]=a[y,x] 并且 a[x,y]+a[y,z]≥a[x,z]。
输出格式
输出一个整数,表示最短 Hamilton 路径的长度。
数据范围
1≤n≤20
0≤a[i,j]≤107
输入样例:
5
0 2 4 5 1
2 0 6 5 3
4 6 0 8 3
5 5 8 0 5
1 3 3 5 0
输出样例:
18
解题思路
考虑两个状态:
1、哪些点被用过
s
t
a
t
e
state
state
2、目前停在哪些点上
j
j
j
f [ s t a t e ] [ j ] = f [ s t a t e k ] [ k ] + w e i g h t [ k ] [ j ] , s t a t e k = s t a t e 除掉 j 之后的集合 f[state][j]=f[state_k][k]+weight[k][j],state_k=state除掉j之后的集合 f[state][j]=f[statek][k]+weight[k][j],statek=state除掉j之后的集合
模板代码
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=20,M=1<<N; // 用n位的二进制数来表示状态集合,如果第i个点已经在集合内,那么第i位对应的数值为1,否则为0。
int f[M][N],w[N][N];//w表示的是无权图
int main()
{
int n;
cin>>n;
// 输入两点之间的权重图
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
cin>>w[i][j];
memset(f,0x3f,sizeof(f));//因为要求最小值,所以初始化为无穷大
f[1][0]=0;//因为零是起点,所以f[1][0]=0;
for(int i=0;i<1<<n;i++)//i表示所有的情况
for(int j=0;j<n;j++)//j表示走到哪一个点
if(i>>j&1)
for(int k=0;k<n;k++)//k表示走到j这个点之前,以k为终点的最短距离
if(i>>k&1)
f[i][j]=min(f[i][j],f[i-(1<<j)][k]+w[k][j]);//更新最短距离
cout<<f[(1<<n)-1][n-1]<<endl;//表示所有点都走过了,且终点是n-1的最短距离
//位运算的优先级低于'+'-'所以有必要的情况下要打括号
return 0;
}
记忆化搜索
901. 滑雪
给定一个 R 行 C 列的矩阵,表示一个矩形网格滑雪场。
矩阵中第 i 行第 j 列的点表示滑雪场的第 i 行第 j 列区域的高度。
一个人从滑雪场中的某个区域内出发,每次可以向上下左右任意一个方向滑动一个单位距离。
当然,一个人能够滑动到某相邻区域的前提是该区域的高度低于自己目前所在区域的高度。
下面给出一个矩阵作为例子:
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9
在给定矩阵中,一条可行的滑行轨迹为 24−17−2−1。
在给定矩阵中,最长的滑行轨迹为 25−24−23−…−3−2−1,沿途共经过 25 个区域。
现在给定你一个二维矩阵表示滑雪场各区域的高度,请你找出在该滑雪场中能够完成的最长滑雪轨迹,并输出其长度(可经过最大区域数)。
输入格式
第一行包含两个整数 R 和 C。
接下来 R 行,每行包含 C 个整数,表示完整的二维矩阵。
输出格式
输出一个整数,表示可完成的最长滑雪长度。
数据范围
1≤R,C≤300,
0≤矩阵中整数≤10000
输入样例:
5 5
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9
输出样例:
25
模板代码
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N=310;
int n,m;
int g[N][N];
int f[N][N];
int dx[4]={-1,0,1,0},dy[4]={0,1,0,-1};
int dp(int x,int y){
int &v=f[x][y];
if(v!=-1) return v;
v=1;
for(int i=0;i<4;i++){
int a=x+dx[i],b=y+dy[i];
if(a>=1 && a<=n && b>=1 &&b<=m && g[x][y]>g[a][b])
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",&g[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;
}
第六讲 贪心
区间问题
905. 区间选点
给定 N 个闭区间 [ai,bi],请你在数轴上选择尽量少的点,使得每个区间内至少包含一个选出的点。
输出选择的点的最小数量。
位于区间端点上的点也算作区间内。
输入格式
第一行包含整数 N,表示区间数。
接下来 N 行,每行包含两个整数 ai,bi,表示一个区间的两个端点。
输出格式
输出一个整数,表示所需的点的最小数量。
数据范围
1≤N≤105,
−10^9≤ai≤bi≤10^9
输入样例:
3
-1 1
2 4
3 5
输出样例:
2
模板代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
struct Range {
int l, r;
bool operator < (const Range& W) const {
return r < W.r;
}
}range[N];
int main() {
scanf("%d", &n);
for (int i = 0; i < n; i++) scanf("%d%d", &range[i].l, &range[i].r);
sort(range, range + n);
int res = 0, ed = -2e9;
for(int i=0;i<n;i++)
if (range[i].l > ed) {
res++;
ed = range[i].r;
}
printf("%d\n", res);
return 0;
}
908. 最大不相交区间数量
给定 N 个闭区间 [ai,bi],请你在数轴上选择若干区间,使得选中的区间之间互不相交(包括端点)。
输出可选取区间的最大数量。
输入格式
第一行包含整数 N,表示区间数。
接下来 N 行,每行包含两个整数 ai,bi,表示一个区间的两个端点。
输出格式
输出一个整数,表示可选取区间的最大数量。
数据范围
1≤N≤105,
−10^9≤ai≤bi≤10^9
输入样例:
3
-1 1
2 4
3 5
输出样例:
2
模板代码
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
struct Range {
int l, r;
bool operator < (const Range& W) const {
return r < W.r;
}
}range[N];
int main() {
scanf("%d", &n);
for (int i = 0; i < n; i++) scanf("%d%d", &range[i].l, &range[i].r);
sort(range, range + n);
int res = 0, ed = -2e9;
for(int i=0;i<n;i++)
if (ed < range[i].l)
{
res++;
ed = range[i].r;
}
printf("%d\n", res);
return 0;
}
906. 区间分组
给定 N 个闭区间 [ai,bi],请你将这些区间分成若干组,使得每组内部的区间两两之间(包括端点)没有交集,并使得组数尽可能小。
输出最小组数。
输入格式
第一行包含整数 N,表示区间数。
接下来 N 行,每行包含两个整数 ai,bi,表示一个区间的两个端点。
输出格式
输出一个整数,表示最小组数。
数据范围
1≤N≤10^5,
−10^9≤ai≤bi≤10^9
输入样例:
3
-1 1
2 4
3 5
输出样例:
2
模板代码
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 100010;
int n;
struct Range {
int l, r;
bool operator < (const Range& W) const {
return l < W.l;
}
}range[N];
int main() {
scanf("%d", &n);
for (int i = 0; i < n; i++) scanf("%d%d", &range[i].l, &range[i].r);
sort(range, range + n);
priority_queue<int, vector<int>, greater<int>> heap;
for (int i = 0; i < n; i++) {
auto r = range[i];
if (heap.empty() || heap.top() > r.l) heap.push(r.r);
else {
heap.pop();
heap.push(r.r);
}
}
printf("%d\n", heap.size());
return 0;
}
907. 区间覆盖
给定 N 个闭区间 [ai,bi] 以及一个线段区间 [s,t],请你选择尽量少的区间,将指定线段区间完全覆盖。
输出最少区间数,如果无法完全覆盖则输出 −1。
输入格式
第一行包含两个整数 s 和 t,表示给定线段区间的两个端点。
第二行包含整数 N,表示给定区间数。
接下来 N 行,每行包含两个整数 ai,bi,表示一个区间的两个端点。
输出格式
输出一个整数,表示所需最少区间数。
如果无解,则输出 −1。
数据范围
1≤N≤10^5,
−10^9≤ai≤bi≤10^9,
−10^9≤s≤t≤10^9
输入样例:
1 5
3
-1 3
2 4
3 5
输出样例:
2
解题思路:
1) 将所有区间按左端点从小到大排序
2) 从前往后依次枚举每个区间,在所有能覆盖start的区间中,选择右端点最大的区间,然后将start更新成右端点的最大值。
模板代码:
#include <algorithm>
#include <iostream>
using namespace std;
const int N=100010;
int n;
struct Range{
int l,r;
bool operator <(const Range& W) const{
return l<W.l;
}
}range[N];
int main(){
int st,ed;
scanf("%d%d",&st,&ed);
scanf("%d",&n);
for(int i=0;i<n;i++){
int l,r;
scanf("%d%d",&l,&r);
range[i]={l,r};
}
sort(range,range+n);
int res=0;
bool success=false;
for(int i=0;i<n;i++){
int j=i,r=-2e9;
while(j<n && range[j].l<=st){
r=max(r,range[j].r);
j++;
}
if(r<st){
res=-1;break;
}
res++;
if(r>=ed){
success=true;
break;
}
st=r;
i=j-1;
}
if(!success) res=-1;
printf("%d\n",res);
return 0;
}
Huffman树
148. 合并果子
在一个果园里,达达已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。
达达决定把所有的果子合成一堆。
每一次合并,达达可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。
可以看出,所有的果子经过 n−1 次合并之后,就只剩下一堆了。
达达在合并果子时总共消耗的体力等于每次合并所耗体力之和。
因为还要花大力气把这些果子搬回家,所以达达在合并果子时要尽可能地节省体力。
假定每个果子重量都为 1,并且已知果子的种类数和每种果子的数目,你的任务是设计出合并的次序方案,使达达耗费的体力最少,并输出这个最小的体力耗费值。
例如有 3 种果子,数目依次为 1,2,9。
可以先将 1、2 堆合并,新堆数目为 3,耗费体力为 3。
接着,将新堆与原先的第三堆合并,又得到新的堆,数目为 12,耗费体力为 12。
所以达达总共耗费体力=3+12=15。
可以证明 15 为最小的体力耗费值。
输入格式
输入包括两行,第一行是一个整数 n,表示果子的种类数。
第二行包含 n 个整数,用空格分隔,第 i 个整数 ai 是第 i 种果子的数目。
输出格式
输出包括一行,这一行只包含一个整数,也就是最小的体力耗费值。
输入数据保证这个值小于 231。
数据范围
1≤n≤10000,
1≤ai≤20000
输入样例:
3
1 2 9
输出样例:
15
模板代码
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
int main() {
int n;
scanf("%d", &n);
priority_queue<int, vector<int>, greater<int>> heap; // 小顶堆
while (n--) {
int x;
scanf("%d", &x);
heap.push(x);
}
int res = 0;
while (heap.size() > 1) {
int a = heap.top(); heap.pop();
int b = heap.top(); heap.pop();
res += a + b;
heap.push(a + b);
}
printf("%d\n", res);
return 0;
}
排序不等式
913. 排队打水
有 n 个人排队到 1 个水龙头处打水,第 i 个人装满水桶所需的时间是 ti,请问如何安排他们的打水顺序才能使所有人的等待时间之和最小?
输入格式
第一行包含整数 n。
第二行包含 n 个整数,其中第 i 个整数表示第 i 个人装满水桶所花费的时间 ti。
输出格式
输出一个整数,表示最小的等待时间之和。
数据范围
1≤n≤105,
1≤ti≤104
输入样例:
7
3 6 1 4 2 5 7
输出样例:
56
模板代码
#include <algorithm>
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 100010;
int n;
int t[N];
int main() {
scanf("%d", &n);
for (int i = 0; i < n; i++)
scanf("%d", &t[i]);
sort(t, t + n);
reverse(t, t + n);
LL res = 0;
for (int i = 0; i < n; i++)
res += t[i] * i;
printf("%lld\n", res);
return 0;
}
绝对值不等式
104. 货仓选址
在一条数轴上有 N 家商店,它们的坐标分别为 A1∼AN。
现在需要在数轴上建立一家货仓,每天清晨,从货仓到每家商店都要运送一车商品。
为了提高效率,求把货仓建在何处,可以使得货仓到每家商店的距离之和最小。
输入格式
第一行输入整数 N。
第二行 N 个整数 A1∼AN。
输出格式
输出一个整数,表示距离之和的最小值。
数据范围
1≤N≤100000,
0≤Ai≤40000
输入样例:
4
6 2 9 1
输出样例:
12
模板代码
#include <algorithm>
#include <iostream>
using namespace std;
const int N = 100010;
int n;
int q[N];
int main() {
scanf("%d", &n);
for (int i = 0; i < n; i++)
scanf("%d", &q[i]);
sort(q, q + n);
int res = 0;
for (int i = 0; i < n; i++)
res += abs(q[i] - q[n / 2]);
printf("%d\n", res);
return 0;
}
125. 耍杂技的牛
农民约翰的 N 头奶牛(编号为 1..N)计划逃跑并加入马戏团,为此它们决定练习表演杂技。
奶牛们不是非常有创意,只提出了一个杂技表演:
叠罗汉,表演时,奶牛们站在彼此的身上,形成一个高高的垂直堆叠。
奶牛们正在试图找到自己在这个堆叠中应该所处的位置顺序。
这 N 头奶牛中的每一头都有着自己的重量 Wi 以及自己的强壮程度 Si。
一头牛支撑不住的可能性取决于它头上所有牛的总重量(不包括它自己)减去它的身体强壮程度的值,现在称该数值为风险值,风险值越大,这只牛撑不住的可能性越高。
您的任务是确定奶牛的排序,使得所有奶牛的风险值中的最大值尽可能的小。
输入格式
第一行输入整数 N,表示奶牛数量。
接下来 N 行,每行输入两个整数,表示牛的重量和强壮程度,第 i 行表示第 i 头牛的重量 Wi 以及它的强壮程度 Si。
输出格式
输出一个整数,表示最大风险值的最小可能值。
数据范围
1≤N≤50000,
1≤Wi≤10,000,
1≤Si≤1,000,000,000
输入样例:
3
10 3
2 5
3 3
输出样例:
2
解题思路
结论:按照
w
i
+
s
i
w_i+s_i
wi+si从小到大的顺序排列,最大的危险系数一定是最小的。
证明:
贪心得到的答案>=最优解
贪心得到的答案<=最优解
假设
w
i
+
s
i
>
w
i
+
1
+
s
i
+
1
w_i+s_i>w_{i+1}+s_{i+1}
wi+si>wi+1+si+1
如果前一个奶牛的s+w的值大于后一个奶牛,那么我们对两个奶牛进行交换
对于上述的每个数减去
w
1
+
w
2
+
.
.
.
w
i
−
1
w_1+w_2+...w_{i-1}
w1+w2+...wi−1
对于上述的每个数加上
s
i
+
s
i
+
1
s_i+s_{i+1}
si+si+1
由于
w
i
+
s
i
>
s
i
w_i+s_i>s_i
wi+si>si 且由假设条件可知,
w
i
+
s
i
>
w
i
+
1
+
s
i
+
1
w_i+s_i>w_{i+1}+s_{i+1}
wi+si>wi+1+si+1
因此
m
a
x
(
s
i
+
1
,
w
i
+
s
i
)
>
m
a
x
(
s
i
,
w
i
+
1
+
s
i
+
1
)
max(s_{i+1},w_i+s_i)>max(s_i,w_{i+1}+s_{i+1})
max(si+1,wi+si)>max(si,wi+1+si+1)
即如果存在前一个奶牛的s+w的值大于后一个奶牛,那么我们对两个奶牛进行交换,交换后的奶牛风险值的最大值变小
模板代码
#include <algorithm>
#include <iostream>
using namespace std;
typedef pair <int, int> PII;
const int N = 50010;
int n;
PII cow[N];
int main() {
scanf("%d", &n);
for (int i = 0; i < n; i++) {
int s, w;
scanf("%d%d", &w, &s);
cow[i] = { w + s,w };
}
sort(cow, cow + n);
int res = -2e9, sum = 0;
for (int i = 0; i < n; i++) {
int s = cow[i].first - cow[i].second, w = cow[i].second;
res = max(res, sum - s);
sum += w;
}
printf("%d\n", res);
}