OI坑
RoderickQiu
2019-02-02 21:18:04
~~感觉不做一个这样的总结帖我迟早得挂。~~
做到了什么整理什么,暂时不打算挖掘远古错误。
# 思路永远是最重要的,优化、特判、反方向操作……
# 0. 论检查的重要性
- 检查变量名是否**写错**,是否把函数当变量,……
- 检查程序是否**符合逻辑**。
- 检查变量是否清零、是否再清零。
- WA之后修改了再提交之前一定要先在**本地做测试**,~~以免影响AC率~~。
- 是否数组开错或开小。
- ……
# 1. DFS
```cpp
void dfs(int steps, int x, int y) {
if (checker()) {
if (steps < minSteps) minSteps = steps;
return;
}
if (steps >= minSteps) return;
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 3; j++) {
if (x != i && y != j) {
allChanger(x, y);
dfs(steps + 1, i, j);
allChanger(x, y);
}
}
}
}
dfs(0, 1, 1);
```
上错下对。
```cpp
void dfs(int steps) {
if (checker()) {
if (steps < minSteps) minSteps = steps;
return;
}
if (steps >= minSteps) return;
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 3; j++) {
allChanger(i, j);
dfs(steps + 1);
allChanger(i, j);
}
}
}
dfs(0);
```
问题所在:
**不要在第0步就限定状态,比如按照上面的做法,就必过一次(1, 1)必然不可取。**
# 2. 01背包与完全背包
01背包的正确写法:
```cpp
void pack01() {
for (int i = 1; i <= n; i++) { //倒序
for (int j = m; j >= 1; j--) {
dp[i] = max(dp[i], dp[i - v[j]] + c[j]);
}
}
}
```
完全背包的正确写法1:
```cpp
void packfull1() {
for (int i = 1; i <= n; i++) {
for (int j = m; j >= 1; j--) { //倒序
for (int k = 1; k <= MAX_K; k++) {
dp[i] = max(dp[i], dp[i - v[j] * k] + c[j] * k);
}
}
}
}
```
完全背包的更优写法:
```cpp
void packfull2() {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) { //正序,可以选择好几次
dp[i] = max(dp[i], dp[i - v[j]] + c[j]);
}
}
}
```
对写法的解释 **(尤其是对循环正序、倒序的那段很重要)**:
> 为什么这个算法就可行呢?首先想想为什么 01 背包中要按照 v 递减的次序来循环。 让 v 递减是为了保证第 i 次循环中的状态 F[i,v] 是由状态 F[i−1,v −Ci] 递推而来。 换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第 i 件物品”这件策 略时,依据的是一个绝无已经选入第 i 件物品的子结果 F[i−1,v−Ci]。而现在完全背 包的特点恰是每种物品可选无限件,所以在考虑“加选一件第 i 种物品”这种策略时, 却正需要一个可能已选入第 i 种物品的子结果 F[i,v−Ci],所以就可以并且必须采用 v 递增的顺序循环。这就是这个简单的程序为何成立的道理。
# 3. Dijkstra的正确链式前向星做法
```cpp
#include <bits/stdc++.h>
#define MAXN 500005 //之前貌似开小了
#define INF 2147483647
using namespace std;
int n, m, s;
struct Edge {
int from, to, val;
} e[MAXN], *p; //使用链式前向星储存(类邻接表解法)
//边编号从1开始,0代表没有下一条边,对应链表的NULL
// hd[i]代表点i边集中的第一条边编号,对应链表的hd指针
// nxt[i]代表边i的下一条边编号,对应链表中的nxt指针
// e[i]代表边i的信息
int hd[MAXN], nxt[MAXN], etop = 1, now;
int d[MAXN]; //从出发点到指定点的最短路径
bool vis[MAXN];
priority_queue< pair< int, int >, vector< pair< int, int > >,
greater< pair< int, int > > >
q; //通过操作,让成对的元素按照元素从小到大的顺序出队,此处使用是为了优化Dijkstra算法每一步中找“最接近的点”的步骤
void insert(int from, int to, int val) {
e[etop].from = from;
e[etop].to = to;
e[etop].val = val;
nxt[etop] = hd[from];
hd[from] = etop++; //单向链表从头部插入
}
void dijkstra(int from) {
d[from] = 0;
q.push(make_pair(0, from)); //将起点加入队列,与自己距离为0
while (!q.empty()) { //类BFS过程
now = q.top().second; // second获得pair的第二项
q.pop();
if (!vis[now]) { //一个访问过的点即无用了,因为一定已经取得最短路了
vis[now] = true;
for (int i = hd[now]; i; i = nxt[i]) { //遍历所有边
p = &e[i];
if (d[p->to] > d[now] + p->val) {
d[p->to] = d[now] + p->val; //松弛操作
q.push(make_pair(d[p->to], p->to)); //进入下一步
}
}
}
}
}
int main() {
scanf("%d %d %d", &n, &m, &s);
for (int i = 0; i <= n; i++) {
d[i] = INF;
}
int f, g, w;
for (int i = 0; i < m; i++) {
scanf("%d %d %d", &f, &g, &w);
insert(f, g, w);
}
dijkstra(s);
for (int i = 1; i <= n; i++) {
cout << d[i] << ' ';
}
return 0;
}
```
尤其是 _**etop要初始化为1**_ ,否则:
```cpp
for (int i = head[now]; i; i = next[i]) { //遍历所有边
p = &e[i];
if (d[p->to] > d[now] + p->val) {
d[p->to] = d[now] + p->val; //松弛操作
q.push(make_pair(d[p->to], p->to)); //进入下一步
}
}
```
不会正常进行。
此外, _**无向图不能使用vis数组加快运算,因为有时需要走回来**_ 。详见[P1339](https://www.luogu.org/problemnew/show/P1339) 。
***另外一个重要的点是:在保存priority_queue的时候,距离永远放在第一个,这是优化的模式所在。***
# 4. Prim
```cpp
#include <bits/stdc++.h>
using namespace std;
struct edge {
int from, to, val;
} e[400005];
int n, m, x, y, z;
int head[5005], next[400005], etop = 1, dis[5005], visitcnt = 0, ans;
bool vis[5005];
priority_queue< pair< int, int >, vector< pair< int, int > >,
greater< pair< int, int > > >
q; //链式前向星
void inserter(int from, int to, int val) {
e[etop].from = from;
e[etop].to = to;
e[etop].val = val;
next[etop] = head[from];
head[from] = etop;
etop++; //标准的链式前向星操作
}
void prim() {
ans = 0; //最小生成树边权值的和
dis[1] = 0;
q.push(make_pair(0, 1)); //随意选取一个点作为已访问集合的第一个点
///距离永远放在第一个,这是priority_queue的优化模式!!!
while (!q.empty() &&
visitcnt <
n) { //只需要访问n-1次就可以完成遍历,之后浪费时间
int now = q.top().second,
nowd = q.top().first; //类似于dijkstra的处理方案
q.pop();
if (!vis[now]) {
vis[now] = true;
ans +=
nowd; ///把这个点加入生成树中,因而是加入当下这个点的dis
visitcnt++;
for (int i = head[now]; i != -1;
i = next[i]) { //遍历相连的,展开到下一步
if (e[i].val <
dis[e[i].to]) { //从堆中找到最小的连接集合内和集合外点的边,将边加入最小生成树中
dis[e[i].to] =
e[i].val; ///和dijkstra的主要区别:Prim的dis是相对于U中的任意一点而言的;而Dijkstra的dis是相对于v0而言的
q.push(make_pair(dis[e[i].to], e[i].to));
///距离永远放在第一个,这是priority_queue的优化模式!!!
}
}
}
}
}
int main() {
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i++) {
vis[i] = false;
dis[i] = INT_MAX;
head[i] = -1;
}
for (int i = 1; i <= m; i++) {
scanf("%d %d %d", &x, &y, &z);
inserter(x, y, z);
inserter(y, x, z); //无向图存两次
}
prim();
if (visitcnt == n)
cout << ans;
else
cout << "orz"; //如果没有遍历完所有的边,则不连通
return 0;
}
```
此处,我们生成的是生成树,因此**Prim的dis是相对于集合中的任意一点而言的**;而Dijkstra的dis是相对于出发点而言的。
# 5. Kruskal与并查集相关
```cpp
#include <bits/stdc++.h>
using namespace std;
struct edge {
int from, to, val;
} e[400005];
int n, m, x, y, z;
int visitcnt = 0, ans = 0, etop = 1; // visitcnt:经过边的数量
int f[400005]; //并查集的操作数组
void inserter(int from, int to, int val) {
e[etop].from = from;
e[etop].to = to;
e[etop].val = val;
etop++;
}
int finder(int x) {
if (f[x] != x)
return f[x] = finder(f[x]);
else
return x;
} //递归法查询最高层父节点,也就是查询在哪个集合中
bool cmp(edge a, edge b) { return a.val < b.val; } //从小到大排序
void kruskal() {
sort(e, e + m,
cmp); //按照边权排序,既然是最小生成树,就要从最小的开始
for (int i = 1; i <= m && visitcnt < n - 1; i++) {
x = finder(e[i].from);
y = finder(e[i].to);
if (x !=
y) { //查询是否已经联通,也就是是否已经在同一个并查集里面
ans += e[i].val;
f[x] = y; //合并
visitcnt++;
}
}
} /* Kruskal:
先把边按照权值进行排序,
用贪心的思想优先选取权值较小的边,
并依次连接,若出现环则跳过此边(生成树没有环)
(用并查集来判断是否存在环)继续搜,
直到已经完全遍历所有的点。*/
int main() {
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i++) {
f[i] = i; ///最开始时,并查集每个元素都单独属于自己
}
for (int i = 1; i <= m; i++) {
scanf("%d %d %d", &x, &y, &z);
inserter(x, y, z); /// kruskal对无向图不需要特殊处理
}
kruskal();
if (visitcnt ==
n - 1) /// visitcnt只会到n-1,因为连结n个点只需要n-1条线
cout << ans;
else
cout << "orz"; //如果没有遍历完所有的边,则不连通
return 0;
}
```
Kruskal使用了并查集。**需要注意的是,Kruskal遍历的是边,而不是点,因此它的visitcnt只能到达n-1,因为连结n个点只需要n-1条线**。
并查集在使用前要先**初始化**:
```cpp
for (int i = 1; i <= n; i++) {
f[i] = i; ///最开始时,并查集每个元素都单独属于自己
}
```
# 6. 线段树
- **写线段树很容易乱,比如搞混用于定位base[]的变量和用于定位tree[]的变量!!**
- **总是从root出发,一层层往下找,比如`change(int l, int r, int now, int target, int val)`,就是当前范围l、r用于base定位,now和lS(now)、rs(now)用于定位tree,target是目标的base值,val是要改成什么。最后通常也输出root。**
- **由于lazytag等,change完了要pushdown或者pushup之类。**