2019春节培训
\(Day1\)
今天講了一些比較基礎的一些可以說是優化暴力吧,因此是非常重要。
搜索
\(DFS\)和\(BFS\)
\(dfs\)——深度優先搜索,用遞歸也就是棧實現,優點是代碼實現簡單,空間耗費少。缺點是不一定能很快的搜到最優解
\(bfs\)——寬度優先搜索,用對列實現,優點是第一個搜到的目標一定是最優解(狹義上的)。缺點是代碼實現復雜,空間耗費大。
所以上面兩種基本搜索需要按題來選擇。
\(Code\)
\(A_{star}\)
\(A_{star}\)是啟發式搜索的一種,其實\(A_{star}\)就是綜合了最良優先搜索和\(Dijkstra\)算法的優點:在使用啟發式算法優化算法效率的時候,保證能得到一個最優解在此算法中,如果以\(g(n)\)表示從起點到任意頂點\(n\)的實際距離, \(h(n)\)表示任意頂點\(n\)到目標頂點的估算距離(根據所采用的評估函數的不同而變化),那么\(A_{star}\)算法的估算函數為:
\[h(n)+g(n)\]
這個公式遵循以下特性:
如果 \(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 edgesif (!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 edgesdfs(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 edgest = 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 \rfloor }_{\lfloor \frac m p \rfloor})*(^{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\n", lucas(n + k, n)); if (lucas(4, 2) == C(4, 2))printf("Yes\n");}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\n", 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,可以說有一點
轉載于:https://www.cnblogs.com/liuwenyao/p/10331744.html
總結
- 上一篇: 统计机器学习第二章 感知机
- 下一篇: 14-jQuery补充