(Day1)
今天讲了一些比较基础的一些可以说是优化暴力吧,因此是非常重要。
搜索
(DFS)和(BFS)
(dfs)——深度优先搜索,用递归也就是栈实现,优点是代码实现简单,空间耗费少。缺点是不一定能很快的搜到最优解
(bfs)——宽度优先搜索,用对列实现,优点是第一个搜到的目标一定是最优解(狭义上的)。缺点是代码实现复杂,空间耗费大。
所以上面两种基本搜索需要按题来选择。
(Code)
void bfs()
{
queue<int> q;
q.push(root);
while (!q.empty())
{
int u = q.front();
q.pop();
work(u);
for (u, v) in edges
q.push(v);
}
}
void dfs(int u)
{
work(u);
for (u, v) in edges
dfs(v);
}
(A_{star})
(A_{star})是启发式搜索的一种,其实(A_{star})就是综合了最良优先搜索和(Dijkstra)算法的优点:在使用启发式算法优化算法效率的时候,保证能得到一个最优解在此算法中,如果以(g(n))表示从起点到任意顶点(n)的实际距离, (h(n))表示任意顶点(n)到目标顶点的估算距离(根据所采用的评估函数的不同而变化),那么(A_{star})算法的估算函数为:
这个公式遵循以下特性:
如果 (g(n))为0,即只计算任意顶点(n)到目标的评估函数(h(n)),而不计算起点到顶点(n)的距离,则算法转化为使用贪心策略的最良优先搜索,速度最快,但可能得不出最优解;
如果(h(n))不大于顶点(n)到目标顶点的实际距离,则一定可以求出最优解,而且(h(n))越小,需要计算的节点越多,算法效率越低,常见的评估函数有——欧几里得距离、曼哈顿距离、切比雪夫距离;
如果(h(n))为0,即只需求出起点到任意顶点(n)的最短路径(g(n)),而不计算任何评估函数(h(n)),则转化为单源最短路径问题,即(Dijkstra)算法,此时需要计算最多的定点;
(Code)
// 假设 q 为一个关于 u 以 f[u] 为关键字的小根堆
g[s] = 0;
f[s] = h(s);
q.push(s);
while (!q.empty() && q.front() != t)
{
int u = q.front();
q.pop();
vis[u] = true;
for (u, v) in edges
if (!vis[v] && g[u] + dis(u, v) < g[v])
{
g[v] = g[u] + dis(u, v);
f[v] = g[v] + h(v);
q.push(v); // 这里也可能是更新 v
}
}
(IDDFS)
特点:DFS 和 BFS 的折中,适用于需要寻找最优解,但没有足够 的内存 BFS 的情况。
(Code)
void iddfs(int u, int d)
{
if (d < 0)
return;
work(u);
for (u, v) in edges
dfs(v, d - 1);
}
for (int d = 1; d <= MAX_DEPTH; ++d)
iddfs(root, d);
(IDA*)
(Code)
int idastar(int u, int g, int maxf)
{
if (u == t)
return FOUND;
if (g + h(u) > maxf)
return g + h(u);
int min = ∞;
for (u, v) in edges
t = idastar(v, g + dis(u, v), maxf);
if (t == FOUND)
return FOUND;
chkmin(min, t);
return min;
}
int maxf = h(s);
while (maxf != FOUND)
maxf = idastar(s, 0, maxf);
(Day2+3)
数论:
积性函数
积性函数是指一个定义域为正整数(n)的算数函数(f(n)),满足(a),(b)互质时,(f(ab)=f(a)*f(b)).
根据定义积性函数一般都是可以用欧拉筛求解的。
组合数
((^n_k)=frac n k(^{n-1}_{k-1})=frac{n-k+1} k(^{~~n}_{k-1})=sum^k_{j=0}(^{n-k-1+j}_{~~~~~~j})=sum^{n-1}_{j=0}(^{~~j}_{k-1})) 。
卢卡斯定理:
((^n_k)equiv(^{lfloor frac n p
floor }_{lfloor frac m p
floor})*(^{n\%p}_{m\%p})\%p)。
根据这个公式的性质,我们可以递归求解一个比较大的((^n_k)\%p),我们暂且称其为(Lucas(n, k, p))
那上面的公式还可以简化,简化为(Lucas(n,k,p)=cm(n\%p,k\%p)×Lucas( frac n p, frac m p ,p))
(Code)(洛谷模板):
#include <bits/stdc++.h>
#define int long long
using namespace std;
int T, n, k, p;
int jie[1000100];
int ksm(int a, int b, int p)
{
int ans = 1;
while (b)
{
if (b & 1)
ans = a * ans %p;
a = a * a % p;
b >>= 1;
}
return ans % p;
}
int f(int a)
{
return jie[a];
}
int ni(int a)
{
return ksm(a, p - 2, p);
}
int C(int n, int k)//求取模之后的组合数
{
if (k > n) return 0;
return ((f(n) * ni(f(k))) % p * ni(f(n - k)) % p);
}
int lucas(int n, int k)
{
if (!k)
return 1;
return lucas(n / p, k / p) * C(n % p, k % p) % p;//我们首先要知道C和lucas两个函数的值是相等的,lucas比较适用于求比较大的组合数mod, 这里我们用n%p和k%p比较小的性质来递归出lucas。
}
signed main()
{
scanf("%lld", &T);
while (T--)
{
scanf("%lld%lld%lld", &n, &k, &p);
jie[0] = 1;//因为0的阶乘不能为1,否则会在求(n-m)!时爆0
jie[1] = 1;
for (int i = 2; i <= p; i++)
jie[i] = jie[i - 1] * i % p;
// printf("%lld
", lucas(n + k, n));
if (lucas(4, 2) == C(4, 2))
printf("Yes
");
}
return 0;
}
不得不说,组合数是非常难的,所以应先把高中数学选修学好。
数据结构
堆
堆有许多种,其中最为经典的且应用比较广泛的便是二叉堆,当然还有其他的堆,比如斐波那契堆等等。
而手写堆和优先队列有很大的区别,比如手写堆比较灵活,且可以随时更改堆中的元素,但是优先队列就没有这些优点,当然代码短是优先队列的优点。
而现在的各种(oi)竞赛中数据结构的重要性逐渐降低,堆则一般和贪心结合在一起。
并查集
并查集简单的有两种常见的优化方法:按秩合并与路径压缩。
难得则有可持久化并查集(ps:可持久化并查集是不能用路径压缩的)。
按秩合并:就是在对两个不同子集连接时,按照rank来连,也就是rank低的连在rank高的下面。rank高的做父亲节点。
路径压缩:向上寻找父亲的路径中经过的所有节点都直接把边连向根节点。
可持久化并查集:首先需要学会可持久化数组,和可持久化线段树。
树状数组
先说一遍,所有可以用树状数组求解的题都可以用线段树来求解,但是树状数组的优点是代码量小好写,常数也小,所以经常被用来求解许多经典的问题。
树状数组主要的思想是二进制的思想,可以用来以(log)的时间来进行维护前缀和或差分数组。
因为这个性质,所以也逐渐衍生了二维树状数组来维护二维前缀和和二维差分数组。
线段树
线段树就支持的操作比较多了,可以说线段树是学习数据结构的第一个门槛。因为他支持的操作远远没有洛谷模板那样简单,真正的线段树不仅仅支持区间修改,区间查询。而线段树主要适用于可以合并的区间问题。也因为它支持的操作多,灵活,所以一位神犇(HJT)发明了主席树(可持久化线段树)。
主席树
主席树是在线数据结构,而离线则可以用整体二分等方法。
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<cmath>
#define mid (l+r)/2
using namespace std;
const int N = 100010, LOG = 20;
int n, m, q, tot = 0;
int a[N], b[N];
int T[N], sum[N*LOG], L[N*LOG], R[N*LOG];
inline int build(int l, int r)
{
int rt = ++ tot;
if (l < r){
L[rt] = build(l, mid);
R[rt] = build(mid+1, r);
}
return rt;
}
inline int update(int pre, int l, int r, int x)
{
int rt = ++ tot;
L[rt] = L[pre]; R[rt] = R[pre]; sum[rt] = sum[pre] + 1;
if (l < r){
if (x <= mid) L[rt] = update(L[pre], l, mid, x);
else R[rt] = update(R[pre], mid+1, r, x);
}
return rt;
}
inline int query(int u, int v, int l, int r, int k)
{
if (l == r) return l;
int x = sum[L[v]] - sum[L[u]];
if (x >= k) return query(L[u], L[v], l, mid, k);
else return query(R[u], R[v], mid+1, r, k-x);
}
int main()
{
scanf("%d%d", &n, &q);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]), b[i] = a[i];
sort(b+1, b+1+n);
m = unique(b+1, b+1+n)-b-1;
T[0] = build(1, m);
for (int i = 1; i <= n; i++)
{
a[i] = lower_bound(b+1, b+1+m, a[i]) - b;
T[i] = update(T[i-1], 1, m, a[i]);
}
while (q--)
{
int x, y, z; scanf("%d%d%d", &x, &y, &z);
int p = query(T[x-1], T[y], 1, m, z);
printf("%d
", b[p]);
}
}
return 0;
}
(Day4)
图论
最小生成树和最短路可以说是比较基础的算法了,当然树论也在此次(noip)中多次涉及,因此学好树论首先要知道一些基本的方法比如基环树。
还有拓扑排序,强连通分量((tarjan)算法)这些在(noip)逐渐超纲的今天已经是基本算法了。
差分约束
差分约束比较特殊,他把代数和图论结合起来了,是用图论方法来解决数学问题的典型例子,而且(noip)还没考过,所以不排除会考的可能。
其可以解决多元一次不等式组的求解。根据松弛然后跑最短路就可以得出最后的解。
树上倍增
树上倍增最为著名的就是(LCA)了,当然也有其他应用范围,(LCA)则是最典型的一个,树上倍增一般都是通过父亲数组的状态转移和(LCA)来求解,说到倍增则不得不需要熟练掌握二进制和位运算。
(example)
(Code)
#include <bits/stdc++.h>
using namespace std;
int fa[2001000][25], lin[2001000], vis[2000100], tot, cnt;
struct edge {
int from, to, nex;
}e[2001000];
inline void add(int a, int b)
{
e[++cnt].from = a;
e[cnt].to = b;
e[cnt].nex = lin[a];
lin[a] = cnt;
}
void dfs(int now, int f)
{
fa[now][0] = f;
for (int i = 1; i <= 20; i++)
fa[now][i] = fa[fa[now][i - 1]][i - 1];
for (int i = lin[now]; i; i = e[i].nex)
{
int to = e[i].to;
if (to == f) continue;
dfs(to, now);
}
}
int sum(int now)
{
int ans = 0;
for (int k = 20; k >= 0; k--)
if (!vis[fa[now][k]])
now = fa[now][k], ans += (1 << k);
return ans;
}
int main()
{
int n, k;
scanf("%d%d", &n, &k);
k = n - k - 1;
for (int i = 1; i <= n - 1; i++)
{
int a, b;
scanf("%d%d", &a, &b); add(a, b), add(b, a);
}
vis[0] = vis[n] = 1;
dfs(n, 0);
for (int i = n; i >= 1; i--)
{
if (vis[i]) continue;//如果已经在树中,就不需要管。
tot = sum(i) + 1; //tot是i到n中经过的节点数
if (tot <= k)
{
k -= tot;
int ha = i;//下面三行是连接路径上的所有节点都要连到树上,共有tot个
for (int j = 0; j <= tot; j++)
vis[ha] = 1, ha = fa[ha][0];
}
}
for (int i = 1; i <= n; i++) if (!vis[i]) printf("%d ", i);
return 0;
}
(Day5)
动态规划
动态规划是(NOIp)最常考的点,而且所分的类别也是有很多的,而且动态规划和其他的算法不一样,其他的算法可以说如果深刻了解了思想基本上就可以了,但是动态规划所需要练习的题目要比其他算法练习的题目要多上许多才可以,因此是非常困难的。
而且要注意的一点是需要掌握许多数据结构和许多方法,比如断环为链,还有许多数据结构和算法优化。因此其实动态规划是最考验一个(OIER)基本素养的考点了。
背包DP
其实背包DP是最简单的动态规划了,因为其实列表就可以得出动态转移方程。
区间DP
这个DP跟线形DP其实也差不了太多,可以用递归的方法来先求出最底层的区间,然后回溯时进行状态转移,从小区间的值来得出大区间的值。
树形DP
其实这也是运用了递归的思想,先求出子树,然后更新根节点的值,就这样不断更新,就能得出结果了。
数位DP
数位DP是比较特殊的DP,直接把数分成几位,然后DP转移的时候直接需要把对每一位分开处理,