ITMO 2022 winter camp
Basic
c++优化
c++会减少相同函数的重复调用
int f(int x)
{
return f(x - 1) + f(x - 1);
}
实际操作与以下写法相同
int f(int x)
{
int c = f(x - 1);
return c + c;
}
关闭输入输出同步
ios::sync_with_stdio(false);
解除cin/cout绑定
cin.tie(0);
以上写法会导致与scanf不同步,cin/cout不同步,在处理问答题的时候慎用。
debug
可以使用以下写法计算程序运行时间
double t = clock();
cout << (clock() - t) / CLOCKS_PER_SEC << endl;
可以使用
cerr //输出流
assert();//错误判断,值为false时会弹出错误
sort
insert sort
算法思想:对每一个数,插入到已排好的数据中去。
复杂度 O(n^2)
for(register int i = 2; i <= n; ++i)
{
for(register int j = i - 1; j >= 1; --j)
{
if(a[j] > a[j + 1])
swap(a[j], a[j + 1]);
else
break;
}
}
merge sort
算法思想:分治。将数列分为左右两侧,递归拆分,再按顺序合并。
复杂度O(nlogn),且稳定为O(nlogn)。
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e5 + 10;
int a[MAXN], b[MAXN], n, p = 1;
void merge(int arr[], int l, int mid, int r)
{
int pl = l, pr = mid;
int ql = mid + 1, qr = r;
while(pl <= pr || ql <= qr)
{
if((ql > qr) || (pl <= pr && arr[pl] <= arr[ql]))
b[p++] = arr[pl++];
else
b[p++] = arr[ql++];
}
while(l <= r)
arr[r--] = b[--p];
}
void mergesort(int arr[], int l, int r)
{
if(l >= r)
return;
int mid = (l + r) >> 1;
mergesort(arr, l, mid);
mergesort(arr, mid + 1, r);
merge(arr, l, mid, r);
}
int main()
{
cin >> n;
for(register int i = 1; i <= n; ++i)
{
cin >> a[i];
}
mergesort(a, 1, n);
for(register int i = 1; i <= n; ++i)
cout << a[i] << " ";
}
逆序对
很简单,在合并的时候,如果右侧数列的数大,那么此时对答案的贡献就是左侧数列中未合并的数的个数。
只需要在merge里加一句话
ans += mid - pl + 1;
这个时候的merge是这样的
void merge(int arr[], int l, int mid, int r)
{
int pl = l, pr = mid;
int ql = mid + 1, qr = r;
while(pl <= pr || ql <= qr)
{
if((ql > qr) || (pl <= pr && arr[pl] <= arr[ql]))
{
b[p++] = arr[pl++];
}
else
{
b[p++] = arr[ql++];
ans += mid - pl + 1;
}
}
while(l <= r)
arr[r--] = b[--p];
}
counting sort
思想:记录每个数出现了几次,从小到大输出。
复杂度:O(n + m)
while(scanf("%d", &x) != EOF)
++cnt[x];
for(register int i = 0; i <= 100; ++i)
{
for(register int j = 1; j <= cnt[i]; ++j)
{
cout << i << " ";
}
}
其他情况
字母:c - 'a'
负数:x + m
大数据:离散化。
数对:另外开p数组,记录.first 为 i 的数对有几个,再分别按照.second排序
Binary Search
lower_bound & upper_bound
实现方法很多,这里记一个好写好用的
int lower_bound(int l, int r, int x)
{
while(l <= r)
{
int mid = (l + r) >> 1;
if(a[mid] < x)
l = mid + 1;
else
r = mid - 1;
}
return l;
}
int upper_bound(int l, int r, int x)
{
while(l <= r)
{
int mid = (l + r) >> 1;
if(a[mid] <= x)
l = mid + 1;
else
r = mid - 1;
}
return l;
}
调用时l,r分别为区间下端点和上端点。
查询数的个数
upper_bound(x) - lower_bound(x) 就可以得到区间内有几个x
lower_bound() & upper_bound() 函数
binary search on real numbers
while(r - l > ESP)
{
if(f(mid) < x)
l = mid;
else
r = mid;
}
但是由于浮点型精度损失,这边建议使用for限制循环次数,而不是使用while
for(register int i = 1; i <= 5000000; ++i)
{
mid = (l + r) / 2;
if(f(mid) < c)
l = mid;
else
r = mid;
}
binary search by answer
二分答案是一个十分重要的解题思路,他需要满足与答案相关的check是在区间上单调可以二分。
\(check() = 0 (x < ans),check() = 1 (x >= ans)\)
这样题目就被简化成考虑如何在有限时间复杂度内完成check操作。
while(l <= r)
{
int mid = (l + r) / 2;
if(check(mid))
r = mid - 1;
else
l = mid + 1;
}
Dynamic Programming
"backword"calculation & "forward"calculation
题目:从i转移到i+1花费b,从i转移到i+2花费c,收益为ai,最大化收益。
backword:
f[i] = max(f[i-1] + a[i] - b, f[i-2] + a[i] - c);
forward:
f[i+1] = max(f[i+1], f[i] + a[i+1] - b);
f[i+2] = max(f[i+2], f[i] + a[i+2] - c);
注意处理好边界
Longest increasing subsequence
f[i] = 1;
f[i] = max(f[i], f[j] + 1); (j < i && a[j] < a[i])
for(int i = 1; i <= n; ++i)
f[i] = 1;
for(int i = 1; i <= n; ++i)
{
for(int j = 1; j < i; ++j)
{
if(a[j] < a[i])
f[i] = max(f[i], f[j] + 1);
}
}
如何记录答案?
store the answer
直接开i个数组,每次转移成功后在g[i]末尾加上a[i];
时间复杂度O(n^3);
store the transition
记录转移,每次转移成功后g[i] = j;
提取答案可以从末尾向前跳转移
//记录答案并记录最末端是哪里
for(register int i = 1; i <= n; ++i)
{
if(f[i] > ans)
{
ans = f[i];
p = i;
}
}
vector<int> A;
A.push_back(p);
//依次向前跳转移
while(p > 0)
{
A.push_back(g[p]);
p = g[p];
}
O(n)
calc the transition
不记录转移,但是在输出方案时计算可行转移。
while(p>0)
{
ans = <a[p]> + ans;
int b = 0;
//找上一个转移
for(int j = 1; j < p; ++j)
{
if(a[j] < a[p] && f[p] == f[j] + 1)
{
b = j;
break;
}
}
p = b;
}
O(n^2)
Non-universal way
使用这种方法需要保证转移的唯一性,不唯一的话是会错的。
while(p>0)
{
for(int j = p-1; j >= 1; --j)
{
if(f[p] == f[j] + 1)
{
b = j;
break;
}
}
p = b;
}
edit distance problem
题目:string s,t。每次花费a在s中增加字符,花费d在s中删去字符,花费r在s中替换字符。求把s变为t的最小花费。
解:f[i][j]表示把s[1-i]变为t[1-j]的最小花费。
转移:
f[i][j] = f[i - 1][j - 1] (s[i] == t[j]);字符相等没有花费
f[i][j] = min(f[i][j], f[i - 1][j] + a);在s[1 - i-1]的基础上加一个字符
f[i][j] = min(f[i][j], f[i][j - 1] + d);在原先序列上减一个字符
f[i][j] = min(f[i][j], f[i - 1][j - 1] + r);在原先序列上替换一个字符
//题目里花费都是1
for(register int i = 0; i <= n; ++i)
{
for(register int j = 0; j <= m; ++j)
{
if(i == 0 && j == 0)
{
f[i][j] = 0;
}
else
{
f[i][j] = n + m + 1;
if(i > 0)
{
f[i][j] = min(f[i][j], f[i - 1][j] + 1);
}
if(j > 0)
{
f[i][j] = min(f[i][j], f[i][j - 1] + 1);
}
if(i > 0 && j > 0)
{
f[i][j] = min(f[i][j], f[i - 1][j - 1] + (s[i - 1] == t[j - ] ? 0 : 1));
}
}
}
}
restoring
在f[i][j]图表中会显示出一个转移路径,从f[n][m] -> f[1][1] 的递减路径。
knapsack Problem
背包十分熟悉了,上代码。记录路径就开一个一样大的数组就好。
for(register int i = 1; i <= n; ++i)
{
for(register int j = m; j >= v[i]; --j)
{
if(f[j] < f[j - v[i]] + c[i])
{
f[j] = f[j - v[i]] + c[i];
vi[i][j] = true;
}
else
vi[i][j] = false;
}
}
for(register int i = 0; i <= m; ++i)
{
if(f[i] > aaa)
{
aaa = f[i];
M = i;
}
}
for(register int i = n; i >= 1; --i)
{
if(vi[i][M])
{
ans.push_back(i);
M -= v[i];
}
}