AcWing算法基础课笔记
理解&会用!
- 时间:代码必须能在1秒钟内跑出来,评测机器1秒钟能运算108~109 次。
- 空间:数组的大小N最大能开到107
pre
stl
-
vector 变长数组,倍增的思想
下标只能用于获取已存在的元素
size() 返回元素个数 O(1)
empty() 返回是否为空
clear() 清空 O(N)
front()/back()
push_back()/pop_back() O(1)
begin()/end()
insert(it, x)向vector中的任意迭代器it处插入一个元素x O(1)
erase(first, last)删除[first, last)内的所有元素
[]
支持比较运算,按字典序 -
set/multiset 自动去重&升序
insert() 插入一个数 O(logN)(在只用到去重时可以用unordered_set提高效率)
find() 查找一个数 O(logN)
count() 返回某一个数的个数
erase()
(1) 输入是一个数x,删除所有x O(k + logn)
(2) 输入一个迭代器,删除这个迭代器
lower_bound()/upper_bound()
lower_bound(x) 返回大于等于x的最小的数的迭代器
upper_bound(x) 返回大于x的最小的数的迭代器 -
string,字符串
size()/length() 返回字符串长度
empty()
clear()
substr(起始下标,(子串长度)) 返回子串
c_str() 返回字符串所在字符数组的起始地址
-
map/multimap
insert() 插入的数是一个pair
erase() 输入的参数是pair或者迭代器
find()
count(key) : 判断键值元素是否存在,若存在返回1,不存在返回0
[] 注意multimap不支持此操作。 时间复杂度是 O(logn)
lower_bound()/upper_bound() -
queue, 队列
size()
empty()
push() 向队尾插入一个元素
front() 返回队头元素
back() 返回队尾元素
pop() 弹出队头元素 -
stack, 栈
size()
empty()
push() 向栈顶插入一个元素
top() 返回栈顶元素
pop() 弹出栈顶元素 -
unordered_set, unordered_map, unordered_multiset, unordered_multimap, 哈希表
和上面类似,增删改查的时间复杂度是 O(1)
不支持 lower_bound()/upper_bound(), 迭代器的++,--
一、基础算法
快排
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int n, a[N];
void quick_sort(int l, int r){
if(l >= r) return;
swap(a[l], a[l + rand() % (r - l + 1)]);//随机选择基准元素
int x = a[l], i = l, j = r;
while(i < j){
while(i < j && a[j] > x) j--;
if(i < j) a[i++] = a[j];
while(i < j && a[i] < x) i++;
if(i < j) a[j--] = a[i];
}
a[i] = x;//把基准元素放中间
quick_sort(l, i - 1);
quick_sort(i + 1, r);
}
int main(){
scanf("%d", &n);
for(int i = 0; i < n; i++) scanf("%d", &a[i]);
quick_sort(0, n - 1);
for(int i = 0; i < n; i++) printf("%d ", a[i]);
return 0;
}
or
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
int n, q[N];
void quick_sort(int l, int r){
if(l >= r) return;
swap(q[l], q[l + rand() % (r - l + 1)]);//防止原序列有序,分配不均
int x = q[l], i = l - 1, j = r + 1;
while(i < j){
while(q[++i] < x);
while(x < q[--j]);
if(i < j) swap(q[i], q[j]);
}
quick_sort(l, j);
quick_sort(j + 1, r);
}
int main(){
scanf("%d", &n);
for(int i = 0; i < n; i++) scanf("%d", q + i);
quick_sort(0, n - 1);
for(int i = 0; i < n; i++) printf("%d ", q[i]);
return 0;
}
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int n, k, a[N];
void quick_sort(int l, int r, int k){
if(l >= r) return;
swap(a[l], a[l + rand() % (r - l + 1)]);
int x = a[l], i = l, j = r;
while(i < j){
while(i < j && a[j] > x) j--;
if(i < j) a[i++] = a[j];
while(i < j && a[i] < x) i++;
if(i < j) a[j--] = a[i];
}
a[j] = x;
int y = i - l + 1;
if(k < y) quick_sort(l, j - 1, k);
else quick_sort(j + 1, r, k - y);
}
int main(){
scanf("%d%d", &n, &k);
for(int i = 1; i <= n; i++) scanf("%d", a + i);
quick_sort(1, n, k);
printf("%d", a[k]);
return 0;
}
归并
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int n, a[N], tmp[N];
void merge_sort(int l, int r){
if(l >= r) return;
int mid = l + r >> 1;
merge_sort(l, mid), merge_sort(mid + 1, r);
int i = l, j = mid + 1, k = l;
while(i <= mid && j <= r){
if(a[i] <= a[j]) tmp[k++] = a[i++];//保持稳定性
else tmp[k++] = a[j++];
}
while(i <= mid) tmp[k++] = a[i++];
while(j <= r) tmp[k++] = a[j++];
for(int i = l; i <= r; i++) a[i] = tmp[i];
}
int main(){
scanf("%d", &n);
for(int i = 0; i < n; i++) scanf("%d", &a[i]);
merge_sort(0, n - 1);
for(int i = 0; i < n; i++) printf("%d ", a[i]);
return 0;
}
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
typedef long long ll;
int n, a[N], tmp[N];
ll merge_sort(int l, int r){
if(l >= r) return 0;
int mid = l + r >> 1, i = l, j = mid + 1, k = l;
ll ans = merge_sort(l, mid) + merge_sort(mid + 1, r);
while(i <= mid && j <= r){
if(a[i] <= a[j]) tmp[k++] = a[i++];
else{
tmp[k++] = a[j++];
ans += mid - i + 1;
}
}
while(i <= mid) tmp[k++] = a[i++];
while(j <= r) tmp[k++] = a[j++];
for(int i = l; i <= r; i++) a[i] = tmp[i];
return ans;
}
int main(){
scanf("%d", &n);
for(int i = 0; i < n; i++) scanf("%d", a + i);
printf("%lld", merge_sort(0, n - 1));
return 0;
}
蓝桥杯:1. 逆序对数
给的例子应该是5个吧
#include <iostream>
using namespace std;
typedef long long ll;
int a[100] = {87,39,35,1,99,10,54,1,46,24,74,62,49,13,2,80,24,58,8,14,83,23,97,85,3,2,86,10,71,15}, tmp[100];
ll merge_sort(int l, int r){
if(l >= r) return 0;
int mid = l + r >> 1, i = l, j = mid + 1, k = l;
ll ans = merge_sort(l, mid) + merge_sort(mid + 1, r);
while(i <= mid && j <= r){
if(a[i] <= a[j]) tmp[k++] = a[i++];
else{
tmp[k++] = a[j++];
ans += mid - i + 1;
}
}
while(i <= mid) tmp[k++] = a[i++];
while(j <= r) tmp[k++] = a[j++];
for(int i = l; i <= r; i++) a[i] = tmp[i];
return ans;
}
int main()
{
int n = 0;
for(int i = 0; i < 100 && a[i] != 0; i++) n++;
printf("%lld", merge_sort(0, n - 1));
return 0;
}
二分
有单调性一定可以二分,可以二分不一定有单调性
→二分的本质不是单调性
789. 数的范围
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int main(){
int n, q, a[N], mid, l, r, x;
scanf("%d%d", &n, &q);
for(int i = 0; i < n; i++) scanf("%d", a + i);
while(q--){
scanf("%d", &x);
int l = 0, r = n - 1;
while(l < r){//寻找第一个等于x的位置
mid = l + r >> 1;
if(a[mid] >= x) r = mid;
else l = mid + 1;
}
if(a[l] != x) printf("-1 -1\n");
else{
printf("%d", l);
l = 0, r = n - 1;
while(l < r){//寻找最后一个等于x的位置
mid = l + r + 1 >> 1;//加 1 是为了避免死循环(当 l = r - 1 时,mid 会等于 l,导致 l 无法更新
if(a[mid] <= x) l = mid;
else r = mid - 1;
}
printf(" %d\n", r);
}
}
return 0;
}
790. 数的三次方根
题干要求"结果保留 6 位小数",判断可加两位小数,即 < 1e-8
#include<iostream>
using namespace std;
int main(){
double n, l = -10000, r = 1e4, mid;
scanf("%lf", &n);
while(l + 1e-8 < r){
mid = (l + r) / 2;
if(mid * mid * mid > n) r = mid;
else l = mid;
}
printf("%.6f", l);
return 0;
}
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int h[N], w[N];
int n, k;
bool check(int p){
int sum = 0;
for(int i = 0; i < n; i++){
sum += (h[i]/p) * (w[i]/p);
}
if(sum >= k) return true;
return false;
}
int main(){
int l = 1, r = 1e5, ans = 1;//边长的可能取值是1~1e5
scanf("%d%d", &n, &k);
for(int i = 0; i < n; i++) scanf("%d%d", &h[i], &w[i]);
while(l <= r){//精确值”或最大/最小满足条件的值取等
int mid = (l + r) / 2;
if(check(mid)){
ans = mid;
l = mid + 1;
}
else r = mid - 1;
}
printf("%d", ans);
return 0;
}
3. 冶炼金属
二分的首次/最后一次出现的模板
#include<iostream>
using namespace std;
const int N = 1e4 + 10;
int a[N], b[N];
int n;
bool check1(int p){//找到a[i] / p >= b[i]里的最后一个值,即v的最小值
for(int i = 0; i < n; i++){
if(a[i] / p < b[i]) return false;
}
return true;
}
bool check2(int p){//找到a[i] / p <= b[i]里的第一个值,即v的最大值
for(int i = 0; i < n; i++){
if(a[i] / p > b[i]) return false;
}
return true;
}
int main(){
int l = 1, r = 1e9, ans1, ans2;
scanf("%d", &n);
for(int i = 0; i < n; i++) scanf("%d%d", &a[i], &b[i]);
while(l < r){//最后一个满足要求的v,即最大的v
int mid = l + (r - l) / 2;
if(check1(mid)){
ans1 = mid;
l = mid + 1;
}
else r = mid;
}
l = 1, r = ans1;
while(l < r){//首个满足要求的v,即最小的v
int mid = l + (r - l) / 2;
if(check2(mid)){
ans2 = mid;
r = mid;
}
else l = mid + 1;
}
printf("%d %d", ans2, ans1);
return 0;
}
对问题本身进行分析
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e4 + 10;
int l[N], r[N];
int main(){
int n, a, b;
scanf("%d", &n);
for(int i = 0; i < n; i++){
scanf("%d%d", &a, &b);
l[i] = a / (b + 1) + 1;
r[i] = a / b;
}
sort(l, l + n);
sort(r, r + n);
printf("%d %d", l[n - 1], r[0]);
return 0;
}
8. 求阶乘
好耶,会用板子了
#include<iostream>
using namespace std;
typedef long long ll;
ll k;
ll have(ll p){
ll ans = 0;
while(p){
ans += p / 5;
p /= 5;
}
return ans;
}
int main(){
scanf("%lld", &k);
ll l = 1, r = 5 * k;
while(l < r){
ll mid = l + (r - l) / 2;
if(have(mid) < k){
l = mid + 1;
}
else r = mid;
}
if(have(r) == k) printf("%lld", r);
else printf("-1");
return 0;
}
高精度
除了加法都有可能形成前导0要处理
123 - 123 = 000
123 * 0 = 000
123 / 456 = 000……123
- 加法
791. 高精度加法
#include<iostream>
#include<vector>
using namespace std;
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() || t; i++){//别漏了可能的最高位进位~
if(i < a.size()) t += a[i];
if(i < b.size()) t += b[i];
c.push_back(t % 10);
t /= 10;
}
return c;
}
int main(){
vector<int> A, B, C;
string s1, s2;
cin >> s1 >> s2;
for(int i = s1.size() - 1; i >= 0; i--) A.push_back(s1[i] - '0');
for(int i = s2.size() - 1; i >= 0; i--) B.push_back(s2[i] - '0');
C = add(A, B);
for(int i = C.size() - 1; i >= 0; i--) printf("%d", C[i]);
}
- 减法
792. 高精度减法
#include<iostream>
#include<vector>
using namespace std;
bool cmp(vector<int> A, vector<int> B){
if(A.size() != B.size()) return A.size() > B.size();//正数相减,不用考虑符号
for(int i = A.size() - 1; i >= 0; i--){
if(A[i] != B[i]) return A[i] > B[i];
}
return true;
}
vector<int> sub(vector<int> A, vector<int> B){
vector<int> C;
int t = 0;
for(int i = 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()) C.pop_back();//去掉多余的零蛋
return C;
}
int main(){
string a, b;
vector<int> A, B, C;
cin >> a >> b;
for(int i = a.size() - 1; i >= 0; i--) A.push_back(a[i] - '0');
for(int i = b.size() - 1; i >= 0; i--) B.push_back(b[i] - '0');
if(cmp(A, B)) C = sub(A, B);
else{
printf("-");
C = sub(B, A);
}
for(int i = C.size() - 1; i >= 0; i--) printf("%d", C[i]);
return 0;
}
- 乘法
793. 高精度乘法
#include<iostream>
#include<vector>
using namespace std;
vector<int> multi(vector<int> A, int b){
vector<int> C;
int t = 0;
for(int i = 0; i < A.size() || t; i++){//别忘了可能的进位~
if(i < A.size()) t += A[i] * b;
C.push_back(t % 10);
t /= 10;
}
while(C.size() > 1 && !C.back()) C.pop_back();//例如12345*0=00000
return C;
}
int main(){
string a;
int b;
vector<int> A, C;
cin >> a >> b;
for(int i = a.size() - 1; i >= 0; i--) A.push_back(a[i] - '0');
C = multi(A, b);
for(int i = C.size() - 1; i >= 0; i--) printf("%d", C[i]);
return 0;
}
- 除法
794. 高精度除法
#include<iostream>
#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());//小端存储时去前导零方便
while(C.size() > 1 && !C.back()) C.pop_back();
return C;
}
int main(){
string a;
vector<int> A, C;
int b, r;
cin >> a >> b;
for(int i = a.size() - 1; i >= 0; i--) A.push_back(a[i] - '0');//小端存储
C = div(A, b, r);
for(int i = C.size() - 1; i >= 0; i--) printf("%d", C[i]);
cout << endl << r;
return 0;
}
引用更快一些
#include<iostream>
#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());
while(C.size() > 1 && !C.back()) C.pop_back();
return C;
}
int main(){
string a;
vector<int> A, C;
int b, r;
cin >> a >> b;
for(int i = a.size() - 1; i >= 0; i--) A.push_back(a[i] - '0');//小端存储
C = div(A, b, r);
for(int i = C.size() - 1; i >= 0; i--) printf("%d", C[i]);
cout << endl << r;
return 0;
}
1. R 格式
AC 50%
#include<iostream>
#include<cmath>
using namespace std;
int main(){
double d, n;
scanf("%lf%lf", &n, &d);
printf("%lld", (long long)(pow(2, n) * d + 0.5));
return 0;
}
高精度方法
#include<iostream>
#include<string.h>
using namespace std;
const int N = 1e7;
struct bign{
int len;
int d[N];
bign(){
len = 0;
memset(d, 0, sizeof(d));
}
}b;
int main(){
string s;
int n, p, l, carry;
cin >> n >> s;
b.len = l = s.size();
for(int i = 0; i < l; i++){
if(s[i] == '.') p = l - i - 1;
else b.d[l - i - 1] = s[i] - '0';//小端存储
}
while(n--){
for(int i = 0; i < l; i++){
if(i == p) continue;
int t = carry + b.d[i] * 2;
b.d[i] = t % 10;
carry = t / 10;
}
while(carry){
b.d[l++] = carry % 10;
carry /= 10;
}
}
carry = (b.d[p - 1] + 5) / 10;
int temp = p + 1;
while(carry){
b.d[temp] = (b.d[temp] + carry) % 10;
carry /= 10;
temp++;
}
if(temp > l) l = temp;
for(int i = l - 1; i > p; i--){
cout << b.d[i];
}
return 0;
}
前缀和和差分
前缀和vs差分,是逆运算的关系
前缀和
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int main(){
int a[N], n, m, x, r, l;
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++){
scanf("%d", &x);
a[i] = a[i - 1] + x;
}
while(m--){
scanf("%d%d", &l, &r);
printf("%d\n", a[r] - a[l - 1]);
}
return 0;
}
#include<iostream>
const int N = 1010;
int s[N][N];
int main(){
int n, m, q, x, x1, x2, y1, y2;
scanf("%d%d%d", &n, &m, &q);
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
scanf("%d", &x);
s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + x;
}
}
while(q--){
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;
}
4. 统计子矩阵
通过70%
#include<iostream>
typedef long long ll;
const int N = 510;
ll s[N][N] = {0};
int main(){
ll n, m, k, x, ans = 0, t;
scanf("%lld%lld%lld", &n, &m, &k);
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
scanf("%lld", &x);
s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + x;//前缀和
}
}
for(int x1 = 1; x1 <= n; x1++){
for(int x2 = x1; x2 <= n; x2++){
for(int y1 = 1; y1 <= m; y1++){
for(int y2 = y1; y2 <= m; y2++){
t = s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1];
if(t <= k) ans++;
}
}
}
}
printf("%lld", ans);
return 0;
}
100%
#include<iostream>
typedef long long ll;
const int N = 510;
ll s[N][N] = {0};
int main(){
ll n, m, k, x, ans = 0, t;
scanf("%lld%lld%lld", &n, &m, &k);
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
scanf("%lld", &x);
s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + x;//前缀和
}
}
for(int x1 = 1; x1 <= n; x1++){
for(int x2 = x1; x2 <= n; x2++){
for(int y1 = 1, y2 = 1; y1 <= m; y1++){//用双指针优化内层循环
while(y2 <= m && s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1] <= k) y2++;//滑动窗口,减少了一个数据,则之前满足<k,现在肯定也能满足<k,省去了冗余判断
ans += y2 - y1;
}
}
}
printf("%lld", ans);
return 0;
}
差分
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int a[N], b[N];
int main(){
int n, m, l, r, c;
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++){
scanf("%d", a + i);//原数组
b[i] = a[i] - a[i - 1];//差分数列
}
while(m--){
scanf("%d%d%d", &l, &r, &c);//[l,r]之间的每个数加上 c
b[l] += c, b[r + 1] -= c;
}
for(int i = 1; i <= n; i++){
a[i] = a[i - 1] + b[i];
printf("%d ", a[i]);
}
return 0;
}
798. 差分矩阵
结合前缀和,会发现是相似的逻辑
#include<iostream>
const int N = 1010;
int a[N][N], b[N][N];
int main(){
int n, m, q, x1, x2, y1, y2, c;
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]);
b[i][j] = a[i][j] - a[i - 1][j] - a[i][j - 1] + a[i - 1][j - 1];
}
}
while(q--){
scanf("%d%d%d%d%d", &x1, &y1, &x2, &y2, &c);
b[x1][y1] += c;
b[x2 + 1][y1] -= c;
b[x1][y2 + 1] -= c;
b[x2 + 1][y2 + 1] += c;
}
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
a[i][j] = b[i][j] - a[i - 1][j - 1] + a[i - 1][j] + a[i][j - 1];
printf("%d ", a[i][j]);
}
printf("\n");
}
return 0;
}
2. 商品库存管理
30%
#include<iostream>
using namespace std;
const int N = 5010;
int a[N], b[N], l[N], r[N];
int main(){
int n, m, ans = 0, cnt;
scanf("%d%d", &n, &m);
for(int i = 0; i < m; i++){
scanf("%d%d", &l[i], &r[i]);
b[l[i]] += 1;
b[r[i] + 1] -= 1;
}
for(int i = 1; i <= n; i++){
a[i] = a[i - 1] + b[i];
if(!a[i]) ans++;
}
for(int i = 0; i < m; i++){
cnt = 0;
for(int j = l[i]; j <= r[i]; j++){
if(a[j] == 1) cnt++;
}
printf("%d\n", ans + cnt);
}
return 0;
}
#include<iostream>
using namespace std;
const int N = 2010;
int b[N][N];
int main(){
int n, m, x1, x2, y1, y2;
scanf("%d%d", &n, &m);
while(m--){
scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
b[x1][y1] ++;
b[x2 + 1][y1] --;
b[x1][y2 + 1] --;
b[x2 + 1][y2 + 1] ++;
}
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++){
b[i][j] += b[i - 1][j - 1] - b[i - 1][j] - b[i][j - 1];
printf("%d", abs(b[i][j]) % 2);
}
printf("\n");
}
return 0;
}
or
#include<iostream>
using namespace std;
const int N = 2010;
int b[N][N];
int main(){
int n, m, x1, x2, y1, y2;
scanf("%d%d", &n, &m);
while(m--){
scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
b[x1][y1] ^= 1;
b[x2 + 1][y1] ^= 1;
b[x1][y2 + 1] ^= 1;
b[x2 + 1][y2 + 1] ^= 1;
}
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++){
b[i][j] ^= b[i - 1][j - 1] ^ b[i - 1][j] ^ b[i][j - 1];
printf("%d", b[i][j]);
}
printf("\n");
}
return 0;
}
双指针
799. 最长连续不重复子序列
如果搭配着看视频的话,强烈建议先理解代码再看视频
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int a[N], h[N];//a存放原数组,h存放维护的滑动窗口中各元素的出现次数
int main(){
int n, ans = 0;
scanf("%d", &n);
for(int i = 0; i < n; i++) scanf("%d", a + i);
for(int r = 0, l = 0; r < n; r++){
h[a[r]]++;//右端对应的元素出现次数加一
while(h[a[r]] > 1){//滑动窗口中含右端元素相同元素时,缩小左端范围,直至不和右端重复,始终能维持一个不含相同元素的滑动窗口
h[a[l]] --;
l++;
}
ans = max(ans, r - l + 1);//更新实现的最大滑动窗口
}
printf("%d", ans);
return 0;
}
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int a[N], b[N];
int main(){
int n, m, x;
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(a[i] + b[j] > x) j--;
if(a[i] + b[j] == x){
printf("%d %d", i, j);
break;
}
}
return 0;
}
2816. 判断子序列
本题中子序列指序列的一部分项按原有次序排列而得的序列,用双指针来按照次序寻找是否第一个序列的元素按照顺序全部在第二个序列中出现
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int a[N], b[N];
int main(){
int n, m, k = 0, l = 0;
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);
while(k < n && l < m){
if(a[k] == b[l]) k++;
l++;
}
puts(k == n ? "Yes" : "No");
return 0;
}
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int N = 1e5 + 10;
vector<int> logs[N];
int main(){
int n, d, k, ts, id, l, r;
scanf("%d%d%d", &n, &d, &k);
while(n--){
scanf("%d%d", &ts, &id);
logs[id].push_back(ts);
}
for(int id = 0; id < N; id++){
if(logs[id].size() < k) continue;
sort(logs[id].begin(), logs[id].end());
l = r = 0;
while(r < logs[id].size()){
if(logs[id][r] - logs[id][l] >= d) l++;//滑动窗口产固定为d - 1
if(r - l + 1 >= k){//滑动窗口中的点赞数
printf("%d\n", id);
break;
}
r++;
}
}
return 0;
}
位运算
求n的二进制中第k位数字: n >> k & 1
#include<iostream>
using namespace std;
int main(){
int n = 6;
printf("%d\n", n & 1);//0
printf("%d\n", n >> 1 & 1);//1
printf("%d\n", n >> 2 & 1);//1
for(int k = 2; k >= 0; k--) printf("%d", (n >> k & 1));//110
return 0;
}
返回n的最后一位1:lowbit(n) = n & -n
#include<iostream>
using namespace std;
int main(){
int n = 6;
printf("%d\n", n & -n);//2 返回n的最后一位1,所谓最后指的是二进制中最右
return 0;
}
#include<iostream>
using namespace std;
int main(){
int n, x, cnt;
scanf("%d", &n);
while(n--){
cnt = 0;
scanf("%d", &x);
for(int i = 0; i < 32; i++){
if(x >> i & 1) cnt++;
}
printf("%d ", cnt);
}
return 0;
}
or
#include<iostream>
using namespace std;
int main(){
int n, x, cnt;
scanf("%d", &n);
while(n--){
cnt = 0;
scanf("%d", &x);
while(x){
x -= x & -x;
cnt++;
}
printf("%d ", cnt);
}
return 0;
}
3. 异或和之和
暴力AC50%
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int a[N];
int main(){
int n;
long long ans = 0, temp;
scanf("%d", &n);
for(int i = 0; i < n; i++){
scanf("%d", a + i);
ans += a[i];
}
for(int i = 0; i < n; i++){
for(int j = i + 1; j < n; j++){
temp = a[i];
for(int k = i + 1; k <= j; k++) temp ^= a[k];
ans += temp;
}
}
printf("%lld", ans);
return 0;
}
离散化
适用场景:对于稀疏数组的压缩
感觉好麻烦,先🕊一下
区间合并
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
typedef pair<int, int> PII;
vector<PII> d, ans;
int main(){
int n, l, r;
scanf("%d", &n);
for(int i = 0; i < n; i++){
scanf("%d%d", &l, &r);
d.push_back({l, r});
}
sort(d.begin(), d.end());
l = d[0].first, r = d[0].second;
for(int i = 1; i < n; i++){
if(d[i].first > r){//找到一个独立区间,计入
l = d[i].first;
r = d[i].second;
ans.push_back({l, r});
}
else r = max(r, d[i].second);
}
printf("%d", ans.size() + 1);//加一的是l和r存储的最后一个区间,没有存到ans中,算数量时勿漏就行
return 0;
}
二、数据结构
KMP
- 真前缀是指一个字符串的所有不包含该字符串本身的前缀
- 假前缀则是指包含整个字符串本身的前缀。
对于字符串 "abab":- 真前缀有: "", "a", "ab", "aba"
- "abab"不是真前缀,因为它包含了整个字符串
KMP的本质是找到最长的相同前后缀
KMP中找的是真前缀,否则可能会产生死循环
动画版讲解
理解的最好方法是自己找个例子自己手动算一下,例如对于模式串abacab手动模拟代码计算next数组的过程
831. KMP字符串
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
char s[N], p[N];
int ne[N];
int main(){
int n, m;
scanf("%d%s%d%s", &n, p, &m, s);
//求next数组
for(int i = 1, j = 0; i < n; i++){//next数组只和模式串有关
while(j && p[i] != p[j]) j = ne[j - 1];//不匹配时,回退到更短的前缀看看
if(p[i] == p[j]) j++;//当下的字符能匹配,最长的前后缀长度加一
ne[i] = j;
}
//kmp绝不后退O(m + n)
for(int i = 0, j = 0; i < m; i++){
while(j && s[i] != p[j]) j = ne[j - 1];
if(s[i] == p[j]) j++;
if(j == n) printf("%d ", i - n + 1);//j==n时才是匹配完了模式串中的所有字符,j是已经匹配完的长度加一
}
return 0;
}
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
int ne[N];
int main(){
string s, p;
int n, m;
cin >> m >> p >> n >> s;
for(int i = 1, j = 0; i < m; i++){//下标从0开始
while(j && p[i] != p[j]) j = ne[j - 1];
if(p[i] == p[j]) j++;
ne[i] = j;
}
for(int i = 0, j = 0; i < n; i++){//对于原串s的遍历一次完成,不后退(通过充分利用模式串的信息实现)
while(j && s[i] != p[j]) j = ne[j - 1];
if(s[i] == p[j]) j++;
if(j == m) printf("%d ", i - m + 1);
}
return 0;
}
3. 不同子串
unordered_set 的插入和查询是 O(1)(平均情况),比 set 的 O(log n) 更快。set和map同理,如果用不到排序可以用unordered
#include<iostream>
#include<unordered_set>
using namespace std;
int main(){
unordered_set<string> ss;
// string str = "aaab";
string str = "0100110001010001";
for(int i = 1; i <= str.size(); i++){//包含字符串本身
for(int j = 0; j + i <= str.size(); j++){
ss.insert(str.substr(j, i));
}
}
// for(auto t : ss) cout << t << endl;
cout << ss.size();//加上本身
return 0;
}
Trie树(字典树)
- 在条件判断中,任何非零值(包括字符)会被视为 true,零值(\0)视为 false
- 全局变量(包括静态数组)默认初始化为零(0、false、nullptr)
- 局部变量(非静态)不会自动初始化,值是未定义的(可能是随机值)。
int main() {
int tr[N][26]; // 局部变量,未初始化,值是随机的
int idx; // 局部变量,未初始化,值是随机的
// 直接使用 tr 和 idx 会导致未定义行为!
return 0;
}
835. Trie字符串统计
统计某个字符串出现过的次数。例如对于be,best,则字符串be只出现过一次
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int tr[N][26], cnt[N], idx;//tr字典树,tr[p][u]记录节点p的子节点u的编号 , cnt[p]记录以节点p结尾的字符串插入次数, idx分配节点的标识
void insert(char *s){
int p = 0;//从根节点开始
for(int i = 0; s[i]; i++){
int u = s[i] - 'a';
if(!tr[p][u]) tr[p][u] = ++idx;//首次出现则动态分配一个新标识
p = tr[p][u];
}
cnt[p]++;
}
int query(char *s){
int p = 0;
for(int i = 0; s[i]; i++){
int u = s[i] - 'a';
if(!tr[p][u]) return 0;
p = tr[p][u];
}
return cnt[p];
}
int main(){
int n;
scanf("%d", &n);
while(n--){
char op[5], str[N];
scanf("%s%s", op, str);
if(op[0] == 'I') insert(str);
else printf("%d\n", query(str));
}
return 0;
}
P8306 【模板】字典树
统计某个前缀出现过的次数,例如对于be,best,则前缀be出现过两次
#include<iostream>
#include<string.h>
using namespace std;
const int N = 3e6 + 10;
int tr[N][62], cnt[N], idx;
char str[N];
int getNum(char s){
int u;
if(s >= '0' && s <= '9') u = s - '0';
else if(s >= 'a' && s <= 'z') u = s - 'a' + 10;
else u = s - 'A' + 36;
return u;
}
void insert(char *s){
int p = 0, u, len = strlen(s);
for(int i = 0; i < len; i++){
u = getNum(s[i]);
if(!tr[p][u]) tr[p][u] = ++idx;
p = tr[p][u];
cnt[p]++;
}
}
int query(char *s){
int p = 0, u, len = strlen(s);
for(int i = 0; i < len; i++){
u = getNum(s[i]);
if(!tr[p][u]) return 0;
p = tr[p][u];
}
return cnt[p];
}
int main(){
int t, n, q;
scanf("%d", &t);
while(t--){
for(int i = 0; i <= idx; i++)
for(int j = 0; j <= 62; j++)
tr[i][j] = 0;
for(int i = 0; i <= idx; i++)
cnt[i] = 0;
idx = 0;
scanf("%d%d", &n, &q);
while(n--){
scanf("%s", str);
insert(str);
}
while(q--){
scanf("%s", str);
printf("%d\n", query(str));
}
}
return 0;
}
#include<iostream>
using namespace std;
const int N = 31e5 + 10;
int tr[N][2], idx;
void insert(int x){
int p = 0;
for(int i = 30; i >= 0; i--){
int u = x >> i & 1;//取x二进制中的第i位
if(!tr[p][u]) tr[p][u] = ++idx;
p = tr[p][u];
}
}
int query(int x){//找到t,满足t和x的异或值最大
int t = 0, p = 0;
for(int i = 30; i >= 0; i--){
int u = x >> i & 1;
if(tr[p][!u]){//优先找位数相反的数,毕竟抑或同0异1,高位的1尽可能多,则x^t的值最大
t = t * 2 + !u;
p = tr[p][!u];
}
else{
t = t * 2 + u;
p = tr[p][u];
}
}
return t;
}
int main(){
int n, x, ans = 0, cur;
scanf("%d", &n);
while(n--){
scanf("%d", &x);
insert(x);
cur = query(x);
ans = max(ans, x^cur);
}
printf("%d", ans);
return 0;
}
三、搜索和图论
BFS
#include<iostream>
#include<queue>
using namespace std;
const int N = 110;
bool visit[N][N];
int a[N][N], flag[N][N];
int X[8] = {0, 0, -1, 1, 1, -1, -1, 1};
int Y[8] = {-1, 1, 0, 0, 1, -1, 1, -1};
int main(){
int n, m;
scanf("%d%d", &n, &m);
for(int i = 0; i < n; i++){
for(int j = 0; j < m; j++){
scanf("%d", &a[i][j]);
}
}
for(int i = 0; i < n; i++){
for(int j = 0; j < m; j++){
if(a[i][j]) flag[i][j] = 9;
else{
int ans = 0;
for(int k = 0; k < 8; k++){
int x = i + X[k], y = j + Y[k];
if(x < 0 || y < 0 || x >= n || y >= m) continue;
ans += a[x][y];
}
flag[i][j] = ans;
}
}
}
for(int i = 0; i < n; i++){
for(int j = 0; j < m; j++){
printf("%d ", flag[i][j]);
}
printf("\n");
}
return 0;
}
#include<iostream>
#include<queue>
#include<map>
#include<string>
#include<algorithm>
using namespace std;
map<string, int> mp;
string s, e;
int n;
int X[6] = {-3, -2, -1, 1, 2, 3};
int bfs(string s){
queue<string> q;
mp[s] = 0;
q.push(s);
while(!q.empty()){
string t = q.front();
q.pop();
if(t == e) return mp[t];
int k = t.find('*');
for(int i = 0; i < 6; i++){
int x = k + X[i];
if(x < 0 || x >= n) continue;
int d = mp[t];
swap(t[k], t[x]);
if(!mp.count(t)){
mp[t] = d + 1;
q.push(t);
}
swap(t[k], t[x]);
}
}
return -1;
}
int main(){
cin >> s >> e;
n = s.size();
cout << bfs(s);
return 0;
}
卡片换位
gets因为不安全已经被弃用,建议用getline
#include <iostream>
#include <string>
using namespace std;
int main() {
string s;
getline(cin, s); // 读取整行,包括空格
cout << "输入的内容: " << s << endl;
return 0;
}
如果之前有 cin >> 操作,需先清除输入缓冲:
cin.ignore(); // 忽略前一个换行符
getline(cin, s);
#include<iostream>
#include<string>
#include<queue>
#include<algorithm>
#include<map>
using namespace std;
int ea, eb, n = 2, m = 3;
int X[4] = {0, 0, -1, 1};
int Y[4] = {-1, 1, 0, 0};
map<string, int> mp;
string s, t;
int bfs(string s){
queue<string> q;
mp[s] = 0;
q.push(s);
while(!q.empty()){
string t = q.front();
q.pop();
if(ea == t.find('A') && eb == t.find('B')) return mp[t];
int k = t.find(' ');
for(int i = 0; i < 4; i++){
int x = k / 3 + X[i], y = k % 3 + Y[i];//一维变二维
if(x < 0 || y < 0 || x >= n || y >= m) continue;
int d = mp[t];
int p = 3*x + y;
swap(t[k], t[p]);
if(!mp.count(t)){
q.push(t);
mp[t] = d + 1;
}
swap(t[k], t[p]);
}
}
}
int main(){
getline(cin, s);
getline(cin, t);
s += t;
eb = s.find('A');
ea = s.find('B');
cout << bfs(s);
return 0;
}
DFS
字典序的排序规则从左到右 依次比较两个序列(如字符串、数组等)的每一个对应位置的元素
#include<iostream>
using namespace std;
const int N = 10;
int path[N], n;
bool st[N];
void dfs(int u){
if(u > n){
for(int i = 1; i <= n; i++) printf("%d ", path[i]);
printf("\n");
}
for(int i = 1; i <= n; i++){
if(!st[i]){
path[u] = i;
st[i] = true;
dfs(u + 1);
st[i] = false;
path[u] = 0;
}
}
}
int main(){
scanf("%d", &n);
dfs(1);
return 0;
}
#include<iostream>
using namespace std;
const int N = 25;
int col[N], diag[N], udiag[N], n;
char path[N][N];
void dfs(int u){
if(u >= n){
for(int i = 0; i < n; i++) puts(path[i]);
printf("\n");
return;
}
for(int i = 0; i < n; i++){
if(!col[i] && !diag[u - i + n] && !udiag[i + u]){
path[u][i] = 'Q';
col[i] = diag[u - i + n] = udiag[i + u] = 1;
dfs(u + 1);
path[u][i] = '.';
col[i] = diag[u - i + n] = udiag[i + u] = 0;
}
}
}
int main(){
scanf("%d", &n);
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
path[i][j] = '.';
}
}
dfs(0);
return 0;
}
#include<iostream>
using namespace std;
int ans;
void dfs(int u, int pre, int sum){
if(sum == 3) return;//剪枝
if(u == 6){
ans++;
return;
}
if(u < 3){
for(int i = 0; i < 16; i++){
if(i == pre) dfs(u + 1, i, sum + 1);
else dfs(u + 1, i, 1);
}
}
else{
for(int i = 0; i < 10; i++){
if(i == pre) dfs(u + 1, i, sum + 1);
else dfs(u + 1, i, 1);
}
}
}
int main(){
dfs(0, -1, 0);
cout << ans;
return 0;
}
14. 五子棋对弈
二维数组坐标转化为线性处理
#include<iostream>
using namespace std;
const int N = 10;
int path[N][N], n = 5, ans;
bool check(){
int t;
for(int i = 0; i < n; i++){//检查行
t = 0;
for(int j = 0; j < n; j++){
t += path[i][j];
}
if(t == 0 || t == n) return false;
}
for(int j = 0; j < n; j++){//检查列
t = 0;
for(int i = 0; i < n; i++){
t += path[i][j];
}
if(t == 0 || t == n) return false;
}
t = 0;
for(int j = 0; j < n; j++){//检查主对角线
for(int i = 0; i < n; i++){
if(i == j) t += path[i][j];
}
}
if(t == 0 || t == n) return false;
t = 0;
for(int j = 0; j < n; j++){//检查次对角线
for(int i = 0; i < n; i++){
if(i + j == n - 1) t += path[i][j];
}
}
if(t == 0 || t == n) return false;
return true;
}
void dfs(int sum, int w){
if(w > 13) return;
if(sum == 25){
if(w == 13 && check()) ans++;
return;
}
int row = sum / 5, col = sum % 5;
path[row][col] = 1;
dfs(sum + 1, w + 1);
path[row][col] = 0;
dfs(sum + 1, w);
}
int main(){
dfs(0, 0);
cout << ans;//3126376
return 0;
}
8. 与或异或
下图中的k对应代码中的i的作用
#include<iostream>
#include<cmath>
using namespace std;
const int N = 5;
int a[N][N];
int main(){
a[0][0] = a[0][2] = a[0][4] = 1;
int ans = 0, all = pow(3, 10), n = 5, op, t;
for(int i = 0; i < all; i++){
t = i;
for(int j = 1; j < n; j++){
for(int k = 0; k < n - j; k++){
op = t % 3;
t /= 3;
if(op == 0) a[j][k] = a[j - 1][k] & a[j - 1][k + 1];
else if(op == 1) a[j][k] = a[j - 1][k] | a[j - 1][k + 1];
else a[j][k] = a[j - 1][k] ^ a[j - 1][k + 1];
}
}
if(a[n - 1][0] == 1) ans++;
}
cout << ans;//30528
return 0;
}
四、数学知识
质数
- 素数(质数):除了1和本身外,不能被其他数整除的数。换言之,对给定的正整数n,若对任意的正整数 a(1<a<n),都有n%a != 0成立,则a时素数
- 合数:除了1和本身外,还存在至少一个能被整除的数。例如4,还能被2整除,则为合数
- 1既不是素数,也不是合数
#include<iostream>
using namespace std;
bool prime(int x){
if(x < 2) return false;
for(int i = 2; i <= x / i; i++){//sqrt(x)需要取到,例如4 = 2*2
if(x % i == 0) return false;
}
return true;
}
int main(){
int n, x;
scanf("%d", &n);
while(n--){
scanf("%d", &x);
if(prime(x)) printf("Yes\n");
else printf("No\n");
}
return 0;
}
or
#include<iostream>
using namespace std;
bool 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, x;
scanf("%d", &n);
while(n--){
scanf("%d", &x);
puts(prime(x) ? "Yes" : "No");
}
return 0;
}
#include<iostream>
using namespace std;
void divide(int x){
for(int i = 2; i <= x / i; i++){
if(x % i == 0){
int cnt = 0;
while(x % i == 0){
x /= i;
cnt++;
}
printf("%d %d\n", i, cnt);
}
}
if(x > 1) printf("%d 1\n", x);
printf("\n");
}
int main(){
int n, x;
scanf("%d", &n);
while(n--){
scanf("%d", &x);
divide(x);
}
return 0;
}
868. 筛质数
埃氏筛法(朴素筛法)
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
int s[N] = {0};
int cnt = 0;
void countPrime(int n){
for(int i = 2; i <= n; i++){
if(s[i]) continue;
cnt++;
for(int j = i + i; j <= n; j += i) s[j] = 1;
}
}
int main(){
int n;
scanf("%d", &n);
countPrime(n);
cout << cnt;
return 0;
}
线性筛法(也称为欧拉筛法)
#include<iostream>
using namespace std;
const int N = 1e6 + 10;
int s[N], primes[N];
int cnt;
void getPrime(int n){
for(int i = 2; i <= n; i++){
if(!s[i]) primes[cnt++] = i;
for(int j = 0; primes[j] <= n / i; j++){
s[i * primes[j]] = 1;//用最小质因数筛掉合数
if(i % primes[j] == 0) break;//i 的最小质因数是 primes[j],后续的 primes[j+1] * i 会被 primes[j] 筛掉,因此不需要继续筛
}
}
}
int main(){
int n;
scanf("%d", &n);
getPrime(n);
cout << cnt;
return 0;
}
8. 超级质数
可看做质数排列组合的数学问题
先分析
一位数:2,3,5,7
两位数:23,37,53,73
三位数:2373 237 ,373,537 737
四位数:none
再用程序判断下
唯一的三位数373
#include <iostream>
using namespace std;
int main()
{
cout << 373;
return 0;
}
约数
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
void getDivisors(int x){
vector<int> ans;
for(int i = 1; i <= x / i; i++){
if(x % i == 0){
ans.push_back(i);
if(i != x / i) ans.push_back(x / i);
}
}
sort(ans.begin(), ans.end());
for(auto t : ans){
printf("%d ", t);
}
printf("\n");
}
int main(){
int n, x;
scanf("%d", &n);
while(n--){
scanf("%d", &x);
getDivisors(x);
}
return 0;
}
870. 约数个数
如果 N = p1^c1 * p2^c2 * ... *pk^ck
#include<iostream>
#include<unordered_map>
using namespace std;
const int P = 1e9 + 7;
typedef long long ll;
unordered_map<int, int> primes;
void getDivisors(int x){
for(int i = 2; i <= x / i; i++){
while(x % i == 0){
x /= i;
primes[i]++;
}
}
if(x > 1) primes[x]++;
}
int main(){
int n, x;
ll ans = 1;
scanf("%d", &n);
while(n--){
scanf("%d", &x);
getDivisors(x);
}
for(auto t : primes) ans = ans * (t.second + 1) % P;
cout << ans;
return 0;
}
871. 约数之和
如果 N = p1^c1 * p2^c2 * ... *pk^ck
约数个数: (c1 + 1) * (c2 + 1) * ... * (ck + 1)
约数之和: (p1^0 + p1^1 + ... + p1^c1) * ... * (pk^0 + pk^1 + ... + pk^ck)
通过分配律展开多项式,生成所有可能的约数,再将它们相加。每一步乘法对应不同质因数的组合,最终结果覆盖了所有约数的总和。
#include<iostream>
#include<unordered_map>
using namespace std;
typedef long long ll;
const int P = 1e9 + 7;
unordered_map<int, int> primes;
void getDivisors(int x){
for(int i = 2; i <= x / i; i++){
while(x % i == 0){
x /= i;
primes[i]++;
}
}
if(x > 1) primes[x]++;
}
int main(){
int n, x;
ll ans = 1, a, b, t;
scanf("%d", &n);
while(n--){
scanf("%d", &x);
getDivisors(x);
}
for(auto temp: primes){
t = 1;
a = temp.first, b = temp.second;
while(b--) t = (t * a + 1) % P;
ans = ans * t % P;
}
cout << ans;
return 0;
}
872. 最大公约数
辗转相除法/欧几里得算法
a和b的最小公倍数=ab/gcd(a, b)=a/(gcd(a,b))*b
其中ab在实际计算时可能会溢出,则进一步优化:先让其中之一除以最大公约数,再乘另一个数。
#include<iostream>
using namespace std;
int gcd(int a, int b){
return b ? gcd(b, a % b) : a;
}
int main(){
int n, a, b;
scanf("%d", &n);
while(n--){
scanf("%d%d", &a, &b);
cout << gcd(a, b) << endl;
}
return 0;
}
#include <iostream>
using namespace std;
int gcd(int a, int b){
return b ? gcd(b, a % b) : a;
}
int main()
{
long long n = 2020, ans = 0;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++){
if(gcd(i, j) == 1) ans++;
}
}
cout << ans;
return 0;
}
7. 等差数列
直接用已有数据的最小公差也能过测试,但其实理论上不合理
#include <iostream>
#include<algorithm>
using namespace std;
const int N = 1e5 + 10;
int a[N];
int main()
{
int n, minD;
scanf("%d", &n);
for(int i = 0; i < n; i++) scanf("%d", a + i);
sort(a, a + n);
minD = a[1] - a[0];
for(int i = 2; i < n; i++){
if(a[i] - a[i - 1] < minD) minD = a[i] - a[i - 1];
}
if(minD == 0) printf("%d", n);
else printf("%d", (a[n - 1] - a[0]) / minD + 1);
return 0;
}
- 本题中正确的最小公差应该是已有数据最小公差的最大公约数,才能实现到所有数据的公差
- 特殊情况,已有数据的最小公差可能为0
#include <iostream>
#include<algorithm>
using namespace std;
const int N = 1e5 + 10;
int a[N];
int gcd(int a, int b){
return b ? gcd(b, a % b) : a;
}
int main()
{
int n, d;
scanf("%d", &n);
for(int i = 0; i < n; i++) scanf("%d", a + i);
sort(a, a + n);
d = a[1] - a[0];
for(int i = 2; i < n; i++){
d = gcd(d, a[i] - a[i - 1]);
}
if(d == 0) printf("%d", n);
else printf("%d", (a[n - 1] - a[0]) / d + 1);
return 0;
}
7. 互质数的个数
暴力30%
#include<iostream>
using namespace std;
typedef long long ll;
const ll P = 998244353;
int gcd(ll a, ll b){
return b ? gcd(b, a % b) : a;
}
int main(){
ll a, b, cnt = 1, x = 1;
cin >> a >> b;
for(int i = 1; i <= b; i++) x *= a;
for(int i = 2; i < x; i++){
if(gcd(x, i) == 1) cnt = (++cnt) % P;
}
cout << cnt;
return 0;
}
10. 约数个数
裸题
#include<iostream>
#include<unordered_map>
using namespace std;
unordered_map<int, int> primes;
int cnt;
void solve(int n){
for(int i = 2; i <= n / i; i++){
if(n % i == 0){
while(n % i == 0){
n /= i;
primes[i]++;
}
}
}
if(n > 1) primes[n]++;
}
int main(){
int n = 1200000, cnt = 1;
solve(n);
for(auto t : primes){
cnt *= (t.second + 1);
}
cout << cnt;
return 0;
}
6. 分数
化简分数用到分子分母的最大公约数
#include<iostream>
#include<cmath>
using namespace std;
typedef long long ll;
struct fract{
ll up, down;
}f, f1;
ll gcd(ll a, ll b){
return b ? gcd(b, a % b) : a;
}
fract add(fract t1, fract t2){
fract t;
t.down = t1.down * t2.down;
t.up = t1.up * t2.down + t2.up * t1.down;
ll redu = gcd(t.up, t.down);
if(redu != 1){
t.up /= redu;
t.down /= redu;
}
return t;
}
int main(){
int n = 20;
f.down = 1;
f.up = 1;
for(int i = 1; i < n; i++){
f1.down = pow(2, i);
f1.up = 1;
f = add(f1, f);
}
printf("%lld/%lld", f.up, f.down);
return 0;
}
等差数列求和
抛开无脑的分数直接计算,分析
结果 = (2^19 + 2^18 + 2 ^ 17 ……+ 2^ 0)/2 ^19 = (2 ^20 - 1)/2^19
#include<iostream>
#include<cmath>
using namespace std;
typedef long long ll;
int main(){
printf("%lld/%lld", (ll)pow(2, 20) - 1, (ll)pow(2, 19));
return 0;
}
4. 无穷分数
类似于斐波那契数列和黄金分割的题目,本以为和分数有关,其实只是借用了分数形式考的递归
#include<iostream>
using namespace std;
int n = 10;
double f(int m){
if(m == n) return 1;
return m / (m + f(m + 1));
}
int main(){
printf("%.05f", f(1));
return 0;
}
#include<iostream>
#include<algorithm>
using namespace std;
int a[15];
int get(int l, int r){
int ans = 0;
for(int i = l; i <= r; i++) ans = ans * 10 + a[i];
return ans;
}
int main(){
int ans = 0, n, x, y, z, power;
scanf("%d", &n);
for(int i = 1; i < 10; i++) a[i] = i;
do{
for(int i = 1; i < 8; i++){
x = get(1, i);
if(x > n) continue;
for(int j = 1; j < 9 - i && i + j < 9; j++){
y = get(i + 1, i + j);
z = get(i + j + 1, 9);
if(y % z) continue;
if(x + y / z == n) ans++;
}
}
}while(next_permutation(a + 1, a + 10));
printf("%d", ans);
return 0;
}
欧拉函数
互质:互质是公约数只有1的两个整数,叫做互质整数。
873. 欧拉函数
#include<iostream>
using namespace std;
int main(){
int n, x, ans;
scanf("%d", &n);
while(n--){
scanf("%d", &x);
ans = x;
for(int i = 2; i <= x / i; i++){
if(x % i == 0){
while(x % i == 0) x /= i;
ans = ans / i * (i - 1) ;
}
}
if(x > 1) ans = ans / x * (x - 1);
cout << ans << endl;
}
return 0;
}
快速幂
#include<iostream>
using namespace std;
typedef long long ll;
int quickPow(ll a, int b, int p){
ll ans = 1 % p;////处理b==0或p == 1的情况
while(b){
if(b & 1) ans = ans * a % p;//计算过程中ans和a可能会超出int
a = a * a % p;
b /= 2;
}
return ans;
}
int main(){
int n, a, b, p;
scanf("%d", &n);
while(n--){
scanf("%d%d%d", &a, &b, &p);
printf("%d\n", quickPow(a, b, p));
}
return 0;
}
or
#include<iostream>
using namespace std;
typedef long long ll;
ll quickpow(ll a, ll b, ll p){
ll ans = 1 % p, t = a;//%p是处理p == 1时能正确返回0;保留原始数据
while(b){
if(b & 1) ans = ans * t % p;
t = t * t % p;
b >>= 1;
}
return ans;
}
int main(){
ll n, a, b, p;
cin >> n;
while(n--){
cin >> a >> b >> p;
cout << quickpow(a, b, p) << endl;
}
return 0;
}
#include<iostream>
using namespace std;
typedef long long ll;
ll quickpow(ll a, ll b, ll p){
ll ans = 1 % p;
while(b){
if(b & 1) ans = ans * a % p;
a = a * a % p;
b /= 2;
}
return ans;
}
int main(){
ll n, a, p;
scanf("%lld", &n);
while(n--){
scanf("%lld%lld", &a, &p);
if(a % p) printf("%lld\n", quickpow(a, p - 2, p));
else printf("impossible\n");
}
return 0;
}
7. 互质数的个数
对于a^b快速幂过程中取模再欧拉数值会发生改变
回想欧拉函数定义,a=p1q1 * p2q2……p3q3,pi均为质数,qi是其幂次
则ab的质因子相同,oula(ab) = a b-1*oula(a) ,本质上是个需要对欧拉函数有一定理解&快速幂模板
#include<iostream>
using namespace std;
typedef long long ll;
const ll P = 998244353;
ll quickpow(ll a, ll b, ll p){
ll ans = 1 % p;
while(b){
if(b & 1) ans = ans * a % p;
a = a * a % p;
b /= 2;
}
return ans;
}
ll oula(ll x){
ll ans = x;
for(int i = 2; i <= x / i; i++){
if(x % i == 0){
while(x % i == 0) x /= i;
ans = ans / i * (i - 1);
}
}
if(x > 1) ans = ans / x * (x - 1);
return ans;
}
int main(){
ll a, b;
scanf("%lld%lld", &a, &b);
printf("%lld", oula(a) * quickpow(a, b - 1, P) % P);
return 0;
}
扩展欧几里得算法
#include<iostream>
using namespace std;
int exgcd(int a, int b, int &x, int &y){
if(!b){
x = 1, y = 0;//a*1 + 0*0 = a
return a;
}
int d = exgcd(b, a % b, y, x);
y -= (a/b) * x;
return d;
}
int main(){
int n, a, b, x, y;
scanf("%d", &n);
while(n--){
scanf("%d%d", &a, &b);
exgcd(a, b, x, y);
printf("%d %d\n", x, y);
}
return 0;
}
#include<iostream>
using namespace std;
int exgcd(int a, int b, int &x, int &y){
if(!b){
x = 1, y = 0;//a*1 + 0*0 = a
return a;
}
int d = exgcd(b, a % b, y, x);
y -= (a/b) * x;
return d;
}
int main(){
int n, a, b, m, d, x, y;
scanf("%d", &n);
while(n--){
scanf("%d%d%d", &a, &b, &m);
d = exgcd(a, m, x, y);
if(b % d) printf("impossible\n");
else printf("%lld\n", (long long) x * (b / d) % m);
}
return 0;
}
中国剩余定理
#include<iostream>
using namespace std;
typedef long long ll;
const int N = 15;
ll a[N], m[N];//余数 ,模数
int n;
ll exgcd(ll a, ll b, ll &x, ll &y){
if(!b){
x = 1, y = 0;//a*1 + 0*0 = a
return a;
}
ll d = exgcd(b, a % b, y, x);
y -= (a/b) * x;
return d;
}
ll crt(){
ll ans = 0, M = 1, x, y, mi;
for(int i = 0; i < n; i++) M *= m[i];
for(int i = 0; i < n; i++){
mi = M / m[i];
x = y = 0;
exgcd(mi, m[i], x, y);//x就是Mi模mi意义下的逆元
ans = ((ans + a[i] * mi * x) % M + M) % M;
}
return ans;
}
int main(){
scanf("%d", &n);
for(int i = 0; i < n; i++) scanf("%lld%lld", m + i, a + i);
printf("%lld", crt());
return 0;
}
高斯消元
组合计数
容斥原理
890. 能被整除的数
(题目讲解视频)G30 容斥原理 集合的并
#include<iostream>
using namespace std;
typedef long long ll;
const int N = 20;
int prime[N];
int main(){
int n, m, t, sign, ans = 0;
scanf("%d%d", &n, &m);
for(int i = 0; i < m; i++) scanf("%d", prime + i);
for(int i = 1; i < 1 << m; i++){
t = 1, sign = -1;
for(int j = 0; j < m; j++){
if(i & 1 << j){//检查第j位是否选中
if((ll) t * prime[j] > n){// 乘积超过n时跳过
t = 0;
break;
}
t *= prime[j];
sign *= -1;//奇数次为负,偶数次为正
}
}
if(t) ans += sign * n / t;
}
cout << ans;
return 0;
}
简单博弈论
891. Nim游戏
反常游戏(Misère Game)按照传统的游戏规则进行游戏,但是其胜者为第一个无法行动的玩家。以 Nim 游戏为例,Nim 游戏中取走最后一颗石子的为胜者,而反常 Nim 游戏中取走最后一刻石子的为败者。
给定N堆物品,第i堆物品有Ai个。两名玩家轮流行动,每次可以任选一堆,取走任意多个物品,可把一堆取光,但不能不取。取走最后一件物品者获胜。两人都采取最优策略,问先手是否必胜。
定理: NIM博弈先手必胜,当且仅当 A1 ^ A2 ^ … ^ An != 0
#include<iostream>
using namespace std;
int main(){
int n, x, ans = 0;
scanf("%d", &n);
while(n--){
scanf("%d", &x);
ans ^= x;
}
if(ans) printf("Yes");
else printf("No");
return 0;
}
892. 台阶-Nim游戏
取走的物品必须从当前堆移动到下一级台阶的堆中。如果当前堆是第k级,取走的物品必须移动到第(k-1)级堆。如果k=1(即最低级),则取走的物品被移出游戏(相当于被取走)
#include<iostream>
using namespace std;
int main(){
int n, x, ans = 0;
scanf("%d", &n);
for(int i = 1; i <= n; i++){
scanf("%d", &x);
if(i & 1) ans ^= x;
}
if(ans) printf("Yes");
else printf("No");
return 0;
}
893. 集合-Nim游戏
3. 异或数列
做题时只能错误分析到最高位奇数个1出现必胜局面,实际还要考虑0的数量。当最高奇数个1只有一个或者是个大于1的奇数个数且0的个数为偶数时才能先手必胜,其余最高奇数个1的情况先手必败。若所有位数的1都是偶数,则为平局。从这题的解法和位置来看,蓝桥杯重思维而不是要手撸复杂的代码
#include<iostream>
const int N = 30;
int main(){
int t, n, x;
scanf("%d", &t);
while(t--){
scanf("%d", &n);
int a[N] = {0}, cnt, ans = 0;
for(int i = 0; i < n; i++){
cnt = 0;
scanf("%d", &x);
while(x){
a[cnt++] += x % 2;
x /= 2;
}
}
for(int j = N - 1; j >= 0; j--){
if(a[j] % 2){
if(a[j] == 1 || n % 2 == 1) ans = 1;
else ans = -1;
break;
}
}
printf("%d\n", ans);
}
return 0;
}
XOOO XXOO OXOO OXXO
OOOO OOOO OOOO OOOO
谁下满了整个棋盘则失败,尝试拆分问题,从第二排来看谁先下谁比败,后手一定可以把第二排上的操作次数变为偶次。
则问题等价于谁后下完第一排谁必胜。情况一二三都可以剩余的棋子变成三次下完,即后手必胜,情况四只有两个棋子必定是剩两次下完,即后手必败
#include <iostream>
using namespace std;
int main()
{
printf("LLLV");
return 0;
}
五、动态规划
事实上呀,如何设计状态和状态转移方程,才是动态规划的核心,而它们也是动态规划最难的地方。
- 常见动态规划问题类型
(1) 最优化问题
求最大值、最小值、最长、最短等。
示例:
最长递增子序列(LIS):求最长上升的子序列长度。
for(int i = 0; i < n; i++){
dp[i] = 1;//默认只有a[i]
for(int j = 0; j < i; j++){
if(a[i] >= a[j] && (dp[j] + 1 > dp[i])) dp[i] = dp[j] + 1;//状态转移方程,用以更新dp[i]
}
ans = max(ans, dp[i]);
}
最长公共子序列(LCS)
for(int i = 1; i < la; i++){
for(int j = 1; j < lb; j++){
if(a[i] == b[j]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
背包问题:在容量限制下最大化价值。
01背包
for(int i = 1; i <= n; i++){//状态转移方程
for(int j = 1; j <= allV; j++){
if(j < w[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);//有两种决策,不放或者放
}
}
for(int i = 1; i <= n; i++){
for(int j = m; j >= w[i]; j--){//滚动数组
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
}
完全背包
for(int i = 1; i <= n; i++){//状态转移方程
for(int j = 1; j <= m; j++){
if(j < w[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i][j - w[i]] + c[i]);
}
}
for(int i = 1; i <= n; i++){
for(int j = w[i]; j <= m; j++){
dp[j] = max(dp[j], dp[j - w[i]] + c[i]);
}
}
最短编辑距离(Edit Distance):计算两个字符串的最小编辑操作次数。
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
if(a[i] == b[j]) dp[i][j] = dp[i - 1][j - 1];//相同则不需要编辑
else dp[i][j] = min(dp[i - 1][j - 1], min(dp[i - 1][j], dp[i][j - 1])) + 1;//状态转移方程分别对应替换,删除,插入
}
}
(2) 计数问题
求所有可能的方案数。
示例:
爬楼梯问题:有多少种方法爬到第n阶?
硬币找零问题:有多少种方式凑出金额 amount?
不同路径问题(Unique Paths):从网格左上角到右下角的路径数。
(3) 可行性问题
判断是否存在某种解。
示例:
子集和问题(Subset Sum):能否从数组中选出若干数使和等于 target?
单词拆分(Word Break):字符串能否由字典中的单词拼接而成?
- 蓝桥杯dp 标签题目
买不到的数目
“不能表示的数”问题(类似“邮票问题”或“货币找零问题”),具体来说:
给定两个正整数 n 和 m,求不能用 n 和 m 的线性组合表示的最大整数(即不能表示为 a·n + b·m,其中 a, b ≥ 0)。
这个最大不能表示的数被称为 Frobenius Number,当 n 和 m 互质时,它存在且等于 n·m - n - m。
#include<iostream>
using namespace std;
const int N = 2e6 + 10;
bool st[N];
int main(){
int n, m;
scanf("%d%d", &n, &m);
for(int i = 0; i <= m; i++){
for(int j = 0; j <= n; j++){
st[i * n + j * m] = true;
}
}
for(int i = n * m - 1; i > 0; i--){
if(!st[i]){
printf("%d", i);
break;
}
}
return 0;
}
质数拆分
01背包
#include<iostream>
#include<string.h>
using namespace std;
const int N = 3e3;
bool st[N];
int prime[N], cnt, dp[N][N];
int main(){
int n = 2022, ans = 0;
for(int i = 2; i <= N && prime[cnt] < n; i++){
if(!st[i]) prime[++cnt] = i;
for(int j = i * i; j <= N; j+=i){
st[j] = true;
}
}
memset(dp, -1, sizeof(dp));
dp[0][0] = 0;
for(int i = 1; i <= cnt; i++){
for(int j = 1; j <= n; j++){
dp[i][j] = dp[i - 1][j];
if(j >= prime[i] && dp[i - 1][j - prime[i]] != -1) dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - prime[i]] + 1);
}
}
cout << dp[cnt][n];
return 0;
}
数字和
数塔问题
#include<iostream>
#include<string>
using namespace std;
const int N = 100;
typedef long long ll;
ll dp[N][N], a[N][N];
int main(){
// int n = 30, m = 60;
// string s[31];
// for(int i = 1; i <= n; i++) {
// cin >> s[i];
// }
// for(int i = 1; i <= n; i++){
// for(int j = 1; j <= m; j++){
// a[i][j] = s[i][j - 1] - '0';
// }
// }
// for(int i = 1; i <= n; i++){
// for(int j = 1; j <= m; j++){
// dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + a[i][j];
// }
// }
// printf("%lld", dp[n][m]);//592
cout << 592;
return 0;
}
#include<iostream>
#include<string>
using namespace std;
const int N = 1010;
int dp[N][N];
int main(){
string s, rs = "";
int ans = 0;
cin >> s;
int len = s.size();
for(int i = 0; i < len; i++){
rs = s[i] + rs;
}
s = " " + s;
rs = " " + rs;
for(int i = 1; i <= len; i++){
for(int j = 1; j <= len; j++){
if(s[i] == rs[j]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
cout << len - dp[len][len];
return 0;
}
reverse(it1, it2) 把[ it1, it2)之间的元素或容器的迭代器在该范围内反转
#include<iostream>
#include<string>
#include<algorithm>
using namespace std;
const int N = 1010;
int dp[N][N];
int main(){
string s, rs;
int ans = 0;
cin >> s;
int len = s.size();
rs = s;
reverse(rs.begin(), rs.end());
s = " " + s;
rs = " " + rs;
for(int i = 1; i <= len; i++){
for(int j = 1; j <= len; j++){
if(s[i] == rs[j]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
cout << len - dp[len][len];
return 0;
}
#include<iostream>
using namespace std;
const int N = 1e5 + 10, M = 110;
int dp[M][N], w[M];//dp[i][j]有前i个砝码能否称出j的重量,0不行,1行 ;w[i]第i个砝码的重量
int main(){
int n, all = 0, ans = 0;
scanf("%d", &n);
for(int i = 1; i <= n; i++){
scanf("%d", w + i);
all += w[i];//所有砝码的总重量是上限
}
for(int i = 1; i <= n; i++){
for(int j = 1; j <= all; j++){
dp[i][j] = dp[i - 1][j];//前i-1个砝码能称出来,加了第i个肯定也行
if(!dp[i][j]){//第i个砝码重量为j || 之前可以凑出来j + w[i] || 之前可以凑出来abs(j-w[i])
if(j == w[i] || dp[i - 1][j + w[i]] || dp[i - 1][abs(j - w[i])]) dp[i][j] = 1;
}
}
}
for(int i = 1; i <= all; i++){
ans += dp[n][i];
}
printf("%d", ans);
return 0;
}
两个互质的数a和b最小不能表示的数就是(a-1)(b-1)-1,也就是说两个互质的数a,b可以表示(a-1)(b-1)之后的所有数字。换言之,给定a,b两个互质的数,他们凑不出来的最大整数为a*b-a-b
呜呜,终于写出动规了≧ ﹏ ≦
#include<iostream>
using namespace std;
const int N = 1e4;
int dp[N], a[N];
int gcd(int a, int b){
return b ? gcd(b, a % b) : a;
}
int main(){
int n, c;
scanf("%d", &n);
for(int i = 1; i <= n; i++){
scanf("%d", a + i);
if(i == 1) c = a[i];
else c = gcd(c, a[i]);
}
if(c > 1) printf("INF");//裴属定理推广:ax+by=gcd(a,b)*n
else{
int ans = 0;
dp[0] = 1;//0肯定能表示出来
for(int i = 1; i <= n; i++){
for(int j = 1; j <= N; j++){//是j规格的蒸笼 || 之前能凑出来j-a[i]
if(j == a[i] || (j - a[i] >= 0 && dp[j - a[i]])) dp[j] = 1;
}
}
for(int i = 1; i <= N; i++){
if(!dp[i]){
ans++;
}
}
printf("%d", ans);
}
return 0;
}
#include<iostream>
using namespace std;
typedef long long ll;
int main(){
int n;
scanf("%d", &n);
ll l = 1, r = n;
while(l < r){
ll mid = l + (r - l)/2;
if(mid * mid + mid < 2 * n) l = mid + 1;
else r = mid;
}
printf("%lld", l);
return 0;
}
#include<iostream>
#include<string>
using namespace std;
typedef long long ll;
int main(){
string s;
int ans = 0;
cin >> s;
int p = 0;
while(p + 1 < s.size()){
if((s[p] == s[p + 1]) || (s[p] == '?' || s[p + 1] == '?')){
ans++;
p += 2;
}
else p++;
}
cout << ans;
return 0;
}
#include<iostream>
using namespace std;
typedef long long ll;
ll ans = 0;
void dfs(int u, int sum){//u已经做过的题数
if(sum == 100 || u > 30) return;
if(sum == 70) ans++;//得到70后,还有可能再归零再次到70 & 得到70且题目数合法才算是一种情况
dfs(u + 1, sum + 10);
dfs(u + 1, 0);
}
int main(){
// dfs(0, 0);
// cout << ans;
cout << 8335366;
return 0;
}
#include<iostream>
using namespace std;
typedef long long ll;
const int N = 110;
int dp[N][N], a[N][N];
int main(){
int n, l = 0, r = 0;
scanf("%d", &n);
for(int i = 1; i <= n; i++){
for(int j = 1; j <= i; j++){
scanf("%d", &a[i][j]);
}
}
dp[1][1] = a[1][1];
for(int i = 2; i <= n; i++){
for(int j = 1; j <= i; j++){
if(j == 1) dp[i][j] = dp[i - 1][j] + a[i][j];
else if(j == i) dp[i][j] = dp[i - 1][j - 1] + a[i][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1]) + a[i][j];//自上而下,a[i][j]的左上a[i-1][j - 1]右上a[i-1][j]
}
}//关于左右选择数相差不大于1,在过程中约束难以实现,可以从结果反推
if(n % 2) printf("%d", dp[n][n/2 + 1]);//左右选择数始终不超过1,则n为奇数时,左右可抵消,一定出现在中间;
else printf("%d", max(dp[n][n/2], dp[n][n/2 + 1])); //n为偶数时,选择中间两个较大者即可(强烈建议画图试试)
return 0;
}
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 100010;
double a[maxn], b[maxn], x[maxn], dp[maxn][2];
int main(){
int n;
scanf("%d", &n);
for(int i = 1; i <= n; i++){//传送杆从1开始
scanf("%lf", x + i);
}
for(int i = 1; i < n; i++){
scanf("%lf%lf", a + i, b + i + 1);
}
//对第一个杆的到达时间初始化
dp[1][0] = x[1];//到1杆下面
dp[1][1] = x[1] + a[1] / 0.7;//到 1杆的传送点
for(int i = 2; i <= n; i++){//依次计算到2杆~n杆的时间开销
if(a[i] < b[i]){//起点比终点高, 需要下爬
dp[i][1] = min(dp[i-1][1] + (b[i] - a[i]) / 1.3, dp[i-1][0] + (x[i] - x[i - 1]) + a[i] / 0.7);
}
//传送门的高度,起点比终点底 ,需要上爬
else dp[i][1] = min(dp[i-1][1] + (a[i] - b[i]) / 0.7, dp[i-1][0] + (x[i] - x[i-1]) + a[i] / 0.7);
dp[i][0] = min(dp[i-1][0] + x[i] - x[i-1], dp[i-1][1] + b[i] / 1.3);
}
printf("%.2f", dp[n][0]);
return 0;
}
or
#include<iostream>
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
double dp[N][2], a[N], b[N], x[N];//dp[i][0]到达第i根竹竿的底部所需时间,dp[i][1] 到达第i根竹竿的传送点所需时间
int main(){
int n;
scanf("%d", &n);
for(int i = 1; i <= n; i++) scanf("%lf", x + i);
for(int i = 1; i < n; i++){
scanf("%lf%lf", &a[i], &b[i + 1]);
}
dp[1][0] = x[1], dp[1][1] = x[1] + a[1] / 0.7;//边界条件:到达第一根杆子的底部和传送点的时间
for(int i = 2; i <= n; i++){
dp[i][0] = min(dp[i - 1][0] + x[i] - x[i - 1], dp[i - 1][1] + b[i] / 1.3);
if(a[i] < b[i]) dp[i][1] = min(dp[i - 1][0] + x[i] - x[i - 1] + a[i] / 0.7, dp[i - 1][1] + (b[i] - a[i]) / 1.3);
else dp[i][1] = min(dp[i - 1][0] + x[i] - x[i - 1] + a[i] / 0.7, dp[i - 1][1] + (a[i] - b[i]) / 0.7);
}
// for(int i = 1; i <= n; i++){
// cout << dp[i][0] << " " << dp[i][1] << endl;
// }
printf("%.2f", dp[n][0]);
return 0;
}
背包问题
2. 01背包问题
[Error] stray '\241' in program
空格也不可以是中文的
#include<iostream>
using namespace std;
const int N = 1010;
int dp[N][N], v[N], c[N];
int main(){
int n, allv;
scanf("%d%d", &n, &allv);
for(int i = 1; i <= n; i++){
scanf("%d%d", v + i, c + i);//体积&价值
}
for(int i = 0; i <= allv; i++) dp[0][i] = 0;//没有物品时,价值为0
for(int i = 1; i <= n; i++){
for(int j = 1; j <= allv; j++){
if(j < v[i]) dp[i][j] = dp[i - 1][j];//体积不够不能放
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + c[i]); //体积足够,选择是否放入
}
}
printf("%d", dp[n][allv]);
return 0;
}
滚动数组
#include<iostream>
using namespace std;
const int N = 1010;
int dp[N], w[N], v[N];
int main(){
int n, m;
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++){
scanf("%d%d", w + i, v + i);
}
for(int i = 1; i <= n; i++){
for(int j = m; j >= w[i]; j--){//// j从背包容量m递减到当前物品重量w,逆序枚举
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
}
printf("%d", dp[m]);
return 0;
}
or
#include<iostream>
using namespace std;
const int N = 1010;
int dp[N], w[N], v[N];
int main(){
int n, m;
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++){
scanf("%d%d", w + i, v + i);
for(int j = m; j >= w[i]; j--){//// j从背包容量m递减到当前物品重量w
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
}
printf("%d", dp[m]);
return 0;
}
#include<iostream>
using namespace std;
const int N = 1010;
int dp[N][N], w[N], v[N];
int main(){
int n, m;
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++) scanf("%d%d", &w[i], &v[i]);
for(int i = 0; i <= m; i++) dp[0][i] = 0;//边界,0件物品的价值一定为0
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
if(j < w[i]) dp[i][j] = dp[i - 1][j];//位置不够不能放
else dp[i][j] = max(dp[i - 1][j], dp[i][j - w[i]] + v[i]);
}
}
printf("%d", dp[n][m]);
return 0;
}
滚动数组
#include<iostream>
using namespace std;
const int N = 1010;
int dp[N], w[N], v[N];
int main(){
int n, m;
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++){
scanf("%d%d", &w[i], &v[i]);
for(int j = w[i]; j <= m; j++){
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
}
printf("%d", dp[m]);
return 0;
}
#include<iostream>
using namespace std;
const int N = 1010;
int dp[N], w, v, s;
int main(){
int n, m;
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++){
scanf("%d%d%d", &w, &v, &s);
for(int j = m; j >= w; j--){
for(int k = 0; k <= s && k * w <= j; k++){
dp[j] = max(dp[j], dp[j - k * w] + k * v);
}
}
}
printf("%d", dp[m]);
return 0;
}
线性DP
#include<iostream>
using namespace std;
const int N = 510;
int a[N][N], dp[N][N];
int main(){
int n;
scanf("%d", &n);
for(int i = 1; i <= n; i++){
for(int j = 1; j <= i; j++){
scanf("%d", &a[i][j]);
if(i == n) dp[n][j] = a[n][j];//最底层节点是边界
}
}
for(int i = n - 1; i > 0; i--){
for(int j = 1; j <= i; j++){
dp[i][j] = max(dp[i + 1][j], dp[i + 1][j + 1]) + a[i][j];
}
}
printf("%d", dp[1][1]);
return 0;
}
#include<iostream>
using namespace std;
const int N = 2e5 + 10;
int main(){
int n;
scanf("%d", &n);
long long a[N], ans = 0, dp[N];
for(int i = 0; i < n; i++){
scanf("%lld", a + i);
}
ans = dp[0] = a[0];
for(int i = 1; i < n; i++){
dp[i] = max(dp[i - 1] + a[i], a[i]);
ans = max(ans, dp[i]);
}
printf("%lld", ans);
return 0;
}
#include<iostream>
using namespace std;
const int N = 1010;
int a[N], dp[N];
int main(){
int n, ans = 1;
scanf("%d", &n);
for(int i = 0; i < n; i++) scanf("%d", a + i);
for(int i = 0; i < n; i++){
dp[i] = 1;
for(int j = 0; j < i; j++){
if(a[j] < a[i]) dp[i] = max(dp[j] + 1, dp[i]);
}
ans = max(dp[i], ans);
}
printf("%d", ans);
return 0;
}
896. 最长上升子序列 II
O(nlogn)
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int a[N], d[N];
int main(){
int n, cnt = 0;
scanf("%d", &n);
for(int i = 0; i < n; i++) scanf("%d", a + i);
for(int i = 0; i < n; i++){
if(!cnt || d[cnt] < a[i]) d[++cnt] = a[i];//首个元素或大于,则可扩展
else if(d[cnt] == a[i]) continue;//同当前最大值,无需处理
else{
int l = 1, r = cnt;
while(l < r){
int mid = (l + r)/2;
if(d[mid] >= a[i]) r = mid;
else l = mid + 1;
}
d[l] = a[i];//用a[i]替换首个大于a[i]的值
}
}
printf("%d", cnt);
return 0;
}
#include<iostream>
#include<string>
using namespace std;
const int N = 1010;
int dp[N][N];
int main(){
string a, b;
int n, m;
cin >> n >> m >> a >> b;
a = " " + a;//从1开始,有利于处理边界条件
b = " " + b;
int la = a.size(), lb = b.size();
for(int i = 0; i < la; i++){
dp[i][0] = 0;
}
for(int i = 0; i < lb; i++){
dp[0][i] = 0;
}
for(int i = 1; i < la; i++){
for(int j = 1; j < lb; j++){
if(a[i] == b[j]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
printf("%d", dp[la - 1][lb - 1]);
return 0;
}
or
#include<iostream>
using namespace std;
const int N = 1010;
int dp[N][N];
char a[N], b[N];
int main(){
int n, m;
scanf("%d%d%s%s", &n, &m, a + 1, b + 1);
for(int i = 0; i <= n; i++){
dp[i][0] = 0;
}
for(int i = 0; i <= m; i++){
dp[0][i] = 0;
}
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
if(a[i] == b[j]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
cout << dp[n][m];
return 0;
}
#include<iostream>
using namespace std;
const int N = 1010;
int dp[N][N];
char a[N], b[N];
int main(){
int n, m;
scanf("%d%s%d%s", &n, a + 1, &m, b + 1);
for(int i = 0; i <= n; i++){//长度为i的A串转化为空串需要i次
dp[i][0] = i;
}
for(int i = 0; i <= m; i++){//空串转为长度为i的B串需要i次
dp[0][i] = i;
}
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
if(a[i] == b[j]) dp[i][j] = dp[i - 1][j - 1];//相同则不需要编辑
else dp[i][j] = min(dp[i - 1][j - 1], min(dp[i - 1][j], dp[i][j - 1])) + 1;//状态转移方程分别对应替换,删除,插入
}
}
printf("%d", dp[n][m]);
return 0;
}
#include<iostream>
#include<string>
using namespace std;
const int N = 1010, M = 15;
string s[N];
int dp[M][M];
int main(){
int n, m;
scanf("%d%d", &n, &m);
for(int i = 0; i < n; i++){
cin >> s[i];
s[i] = " " + s[i];
}
while(m--){
string q;
int ans = 0, num;
cin >> q >> num;
q = " " + q;
for(int i = 0; i < n; i++){
for(int j = 0; j < M; j++){
for(int k = 0; k < M; k++){
dp[j][k] == 0;
}
}
int len2 = q.size(), len1 = s[i].size();
for(int j = 0; j < len1; j++) dp[j][0] = j;
for(int j = 0; j < len2; j++) dp[0][j] = j;
for(int j = 1; j < len1; j++){
for(int k = 1; k < len2; k++){
if(s[i][j] == q[k]) dp[j][k] = dp[j - 1][k - 1];
else dp[j][k] = min(dp[j - 1][k - 1], min(dp[j - 1][k], dp[j][k - 1])) + 1;//替换,删除,插入
}
}
if(dp[len1 - 1][len2 - 1] <= num) ans++;
}
cout << ans << endl;
}
return 0;
}
#include<iostream>
#include<set>
#include<vector>
using namespace std;
int main(){
set<int> si;
int cnt = 1, n = 2020, a[6] = {1, 2, 3, 5, 8};
for(int i = 0; i < 5; i++) si.insert(a[i]);
for(auto i : si){
si.insert(3*i+ 2);
si.insert(5*i + 3);
si.insert(8*i + 5);
if(si.size() >= n*2) break;//多试一些解决小数字在后面出现的情况,或者可以用优先队列每次用最小的数据运算
}
for(auto i : si){
if(cnt == n){
printf("%d", i);//41269
break;
}
cnt++;
}
return 0;
}
2. 耐摔指数
在最优策略下,找到最坏情况下的测试次数
#include<iostream>
using namespace std;
const int N = 1e4 + 10, M = 5;
int dp[M][N];//dp[i][j]表示用i台手机测试j层高度的测试次数
int main(){
int n;
scanf("%d", &n);
for(int i = 1; i <= n; i++) dp[1][i] = i;//一台手机, 只有从低层开始逐步提高测试
for(int i = 2; i <= 3; i++){//有2台和3台手机
for(int j = 1; j <= n; j++){//测试j层高度
dp[i][j] = j;//最坏运气的初始化
for(int k = 1; k < j; k++){
//min采用最优策略, max满足最坏情况, dp[i-1][k-1]第i台摔坏后,用剩下的i-1台测试k-1层 ;dp[i][k+1]第i台手机没坏,有 i台手机测试j-k层
dp[i][j] = min(dp[i][j], max(dp[i - 1][k - 1], dp[i][j - k]) + 1);
}
}
}
printf("%d", dp[3][n]);
return 0;
}
区间DP
适用于所有类似的相邻合并问题(如矩阵链乘法、最优二叉搜索树等)。
#include<iostream>
#include<string.h>
using namespace std;
const int N = 310;
int dp[N][N], s[N];//dp[i][j]表示合并从i到j区间的代价
int main(){
int n, x;
scanf("%d", &n);
for(int i = 1; i <= n; i++){
scanf("%d", &x);
s[i] = s[i - 1] + x;
}
memset(dp, 127, sizeof(dp));
for(int i = 1; i <= n; i++) dp[i][i] = 0;//只有一堆 无需移动
for(int len = 1; len <= n; len++){//区间长度
for(int l = 1; l + len - 1 <= n; l++){//左边界
int r = l + len - 1;//右边界
for(int k = l; k < r; k++){//分界点
dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r] + s[r] - s[l - 1]);
}
}
}
printf("%d", dp[1][n]);
return 0;
}
更小的数
暴力AC40%
#include<iostream>
#include<string>
#include<algorithm>
using namespace std;
typedef long long ll;
int main(){
string s;
int ans = 0;
cin >> s;
for(int i = 0; i < s.size(); i++){
for(int j = 1; j + i <= s.size(); j++){
string sub = s.substr(i, j), rs = sub;
reverse(rs.begin(), rs.end());
if(rs < sub) ans++;
}
}
cout << ans;
return 0;
}
计数类DP
#include<iostream>
using namespace std;
const int P = 1e9 + 7, N = 1010;
int dp[N][N];//dp[i][j]是对于i种选择,j个容量下的方案数
int main(){
int n;
scanf("%d", &n);
for(int i = 0; i <= n; i++) dp[i][0] = 1;//总和为 0 时,只有 “不选任何数” 一种方案(空集)
for(int i = 1; i <= n; i++){ //不同数字视为无限可取的物品
for(int j = 1; j <= n; j++){
if(j < i) dp[i][j] = dp[i - 1][j];
else dp[i][j] = (dp[i - 1][j] + dp[i][j - i]) % P;
}
}
printf("%d", dp[n][n]);
return 0;
}
滚动数组
#include<iostream>
using namespace std;
const int P = 1e9 + 7, N = 1010;
int dp[N];//dp[j]是j个容量下的方案数
int main(){
int n;
scanf("%d", &n);
dp[0] = 1;
for(int i = 1; i <= n; i++){ //不同数字视为无限可取的物品
for(int j = 1; j <= n; j++){
dp[j] = (dp[j] + dp[j - i]) % P;
}
}
printf("%d", dp[n]);
return 0;
}
数位统计DP
状态压缩DP
树形DP
记忆化搜索
六、贪心
区间问题
AcWing908. 最大不相交区间数量
右端点结束越早的区间越优先选择
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e5 + 10;
struct line{
int l, r;
bool operator < (line &a){//按右端点排序
return r < a.r ;
}
}lines[N];
int main(){
int n, last = -1e9-1, ans = 0;
scanf("%d", &n);
for(int i = 0; i < n; i++) scanf("%d%d", &lines[i].l, &lines[i].r);
sort(lines, lines + n);//sort(待排序的首地址,待排序的最后一个地址的下一个位置,比较规则选填)
for(int i = 0; i < n; i++){
if(lines[i].l > last){
ans++;
last = lines[i].r;
}
}
printf("%d", ans);
return 0;
}
AcWing905. 区间选点
其实两个问题求解的是同一个数值,只是从两个角度理解
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e5 + 10;
struct line{
int l, r;
bool operator < (line &a){//按右端点排序
return r < a.r ;
}
}lines[N];
int main(){
int n, last = -1e9 - 1, ans = 0;
scanf("%d", &n);
for(int i = 0; i < n; i++) scanf("%d%d", &lines[i].l, &lines[i].r);
sort(lines, lines + n);//sort(待排序的首地址,待排序的最后一个地址的下一个位置,比较规则选填)
for(int i = 0; i < n; i++){
if(lines[i].l > last){
ans++;
last = lines[i].r;
}
}
printf("%d", ans);
return 0;
}
AcWing906. 区间分组
AcWing907. 区间覆盖
每次取可以覆盖到左端点且最长的区间
- 若不能覆盖左端点,ans = 1,退出
- 若能覆盖左端点,则把当前待覆盖的左端点更新为新区间的r
- 判断此时是否已经完成[s, t]的覆盖,若已完成,退出
- 可能只覆盖一段区间,但不能覆盖到右侧部分的情况
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e5 + 10;
struct line{
int l, r;
bool operator < (line &a){
return l < a.l;
}
}lines[N];
int main(){
int s, t, n, ans = 0;
scanf("%d%d%d", &s, &t, &n);
for(int i = 0; i < n; i++) scanf("%d%d", &lines[i].l, &lines[i].r);
sort(lines, lines + n);
for(int i = 0; i < n; i++){
int temp = i, r = -1e9 - 1;
while(temp < n && lines[temp].l <= s){
r = max(r, lines[temp].r);
temp++;
}
ans++;
if(r < s){
ans = -1;
break;
}
s = r;
if(s >= t){
break;
}
i = temp - 1;
}
if(s < t) ans = -1;
printf("%d", ans);
return 0;
}
Huffman树
#include<iostream>
#include<queue>
using namespace std;
int main(){
int n, x;
long long ans= 0;
priority_queue<int, vector<int>, greater<int> > q;
scanf("%d", &n);
while(n--){
scanf("%d", &x);
q.push(x);
}
while(q.size() > 1){
int x = q.top();
q.pop();
int y = q.top();
q.pop();
ans += x + y;
q.push(x + y);
}
printf("%lld", ans);
return 0;
}
排序不等式
AcWing913. 排队打水
最短进程优先等待时间最短
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
int a[N];
int main(){
ll ans = 0;
int n;
scanf("%d", &n);
for(int i = 1; i <= n; i++) scanf("%d", a + i);
sort(a + 1, a + n + 1);
for(int i = 1; i < n; i++){
ans += (n - i)*a[i];
}
printf("%lld", ans);
return 0;
}
绝对值不等式
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e5 + 10;
int a[N];
int main(){
int n;
long long ans = 0, d;
scanf("%d", &n);
for(int i = 0; i < n; i++) scanf("%d", a + i);
sort(a, a + n);
for(int j = 0; j < n; j++){
d += abs(i - a[n/2]);
}
printf("%lld", ans);
return 0;
}
推公式
AcWing125. 耍杂技的牛
注意输出的是单个最大风险值的最小可能值
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e4 + 10;
struct cow{
int w, s;
}cows[N];
bool cmp(cow c1,cow c2){
if(c1.w + c1.s != c2.w + c2.s) return c1.w + c1.s < c2.w + c2.s;
return c1.s < c2.s;
}
int main(){
int n, s;
long long ans = -2e9, cur = 0;
scanf("%d",&n);
for(int i = 0; i < n; i++){
scanf("%d%d", &cows[i].w, &cows[i].s);
}
sort(cows, cows + n, cmp);
for(int i = 0; i < n; i++){
ans = max(ans, cur - cows[i].s);
cur += cows[i].w;
}
printf("%lld", ans);
return 0;
}
pair用的非常优雅
#include<iostream>
#include<algorithm>
#include<map>
using namespace std;
const int N = 5e4 + 10;
pair<int, int> pii[N];
int main(){
int n, w, s;
long long ans = -2e9, cur = 0;
scanf("%d",&n);
for(int i = 0; i < n; i++){
scanf("%d%d", &w, &s);
pii[i] = {w + s, s};
}
sort(pii, pii + n);
for(int i = 0; i < n; i++){
ans = max(ans, cur - pii[i].second);
cur += pii[i].first - pii[i].second;
}
printf("%lld", ans);
return 0;
}
由于每次操作只影响当前位和下一位,并且前面的字符已经匹配,所以贪心策略能保证最终 a 变成 b
#include<iostream>
using namespace std;
int main(){
string a, b;
int ans = 0;
cin >> a >> b;
for(int i = 0; i < a.size(); i++){
if(a[i] != b[i]){
a[i] = b[i];
if(i + 1 < a.size()){
if(a[i + 1] == '*') a[i + 1] = 'o';
else a[i + 1] = '*';
}
ans++;
}
}
cout << ans;
//cout << endl << a << endl << b;
return 0;
}
17. X 进制减法
(a % p - b % p) % p 和 (a % p - b % p) 只有在 a % p ≥ b % p 时才相等,否则不相等
虽然标签是贪心,但是考点更多的是进制转换&高精度减法
X进制中以321分别是8、10、2进制,转化为十进制3 * 10 * 2 + 2 * 2 + 1 = 65
#include<iostream>
using namespace std;
typedef long long ll;
const int N = 1e5 + 10, P = 1000000007;
int a[N], b[N];
int main(){
ll n, ma, mb, ans = 0, power = 1, cur, carry = 0, op;
scanf("%lld%lld", &n, &ma);
for(int i = ma - 1; i >= 0; i--) scanf("%d", a + i);
scanf("%lld", &mb);
for(int i = mb - 1; i >= 0; i--) scanf("%d", b + i);
for(int i = 0; i < ma; i++){
op = max(max(a[i] + 1, b[i] + 1), 2);
cur = a[i] - b[i] + carry;
if(cur < 0) cur += op;
ans = (ans + cur * power) % P;
power = power * op % P;
if(a[i] + carry < b[i]) carry = -1;
else carry = 0;
}
printf("%lld", ans);
return 0;
}
#include<iostream>
#include<queue>
using namespace std;
const int N = 1e5 + 10;
priority_queue<int, vector<int>, greater<int> > q[10];
int main(){
int n, a, b;
long long ans = 0;
scanf("%d", &n);
for(int i = 0; i < n; i++){
scanf("%d%d", &a, &b);
q[a].push(b);
}
n /= 10;
for(int i = 0; i < 10; i++){
while(q[i].size() > n){
ans += q[i].top();
// printf("%d %d\n", i, q[i].top());
q[i].pop();
}
}
printf("%lld", ans);
return 0;
}