注:题解仅为个人理解
A题: 题目难度排序 O ( n log n ) O(n\log n) O(nlogn)
考察知识:排序
解析:将题目按照题目难度从小到大进行排序。
这道题只要会时间复杂度为
O
(
n
log
n
)
O(n \log n)
O(nlogn)的排序算法就能解决。手搓快速排序算法比较有难度,归并排序较简单,当然也可以借用stl中的sort排序。
小彩蛋:据说本次比赛的难度排序就是A题样例二的结果
以下是手搓的快速排序算法和sort排序的参考代码:
//手写
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 1e6 + 10;
int n, q[N][2];
bool cmp(int i, int j) {
if (q[i][1] == q[j][1]) return q[i][0] < q[j][0];
else return q[i][1] < q[j][1];
}
void quick_sort(int l, int r) {
if (l >= r) return;
q[n][0] = q[l+r>>1][0], q[n][1] = q[l+r>>1][1];
int i = l - 1, j = r + 1;
do {
i++, j--;
while (cmp(i, n)) i++;
while (cmp(n, j)) j--;
if (i < j) swap(q[i], q[j]);
} while (i < j);
quick_sort(l, j);
quick_sort(j + 1, r);
}
int main() {
cin >> n;
for (int i = 0; i < n; i++) scanf("%d", &q[i][0]);
for (int i = 0; i < n; i++) scanf("%d", &q[i][1]);
quick_sort(0, n - 1);
for (int i = 0; i < n; i++) printf("%d ", q[i][0]);
return 0;
}
//stl
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 1e6 + 10;
struct node {
int id, d;
} p[N];
bool cmp (node i, node j) {
if (i.d == j.d) return i.id < j.id;
else return i.d < j.d;
}
int main() {
int n;
cin >> n;
for (int i = 0; i < n; i++) scanf("%d", &p[i].id);
for (int i = 0; i < n; i++) scanf("%d", &p[i].d);
sort(p, p + n, cmp);
for (int i = 0; i < n; i++) printf("%d ", p[i].id);
return 0;
}
B题: 派蒙的订单号 O ( n ) O(n) O(n)
考察知识:推理能力
解析:求上一个全排列。可以推理,可以用stl中的prev_permutation()函数。用stl的话就非常简单了,出题人是想考选手的推理能力的。接下来我简要说说手写的方法,具体为什么这样可行,这里不做解释。
1.首先从右到左找到第一个不满足单调递增的数,记下它的下标为t1;
2.然后再次从右到左找到第一个小于下标t1对应的数的数,记下它的下标为t2;
3.将下标为t1的数与下标为t2的数进行交换;
4.将t1后面的所有数(不包括t1)从大到小的顺序输出。
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 1e6 + 10;
int n, q[N];
int main() {
cin >> n;
for (int i = 1; i <= n; i++) scanf("%d", &q[i]);
int t1, t2;
for (int i = n - 1; i >= 1; i--)
if (q[i] > q[i+1]) {
t1 = i;
break;
}
for (int i = n; i >= 1; i--)
if (q[i] < q[t1]) {
t2 = i;
break;
}
swap(q[t1], q[t2]);
for (int i = 1; i <= t1; i++) printf("%d ", q[i]);
for (int i = n; i > t1; i--) printf("%d ", q[i]);
return 0;
}
C题: 没有校园网的那些日子 O ( n m ) O(nm) O(nm)
考察知识:数组
解析:求在某点的最好信号为多少
题目的数据非常友好, 每次天依换一个位置,我们只要遍历一下所有基站,找出信号最好的基站即可。 时间复杂度 O ( n m ) O(nm) O(nm) 就能AC。
参考代码:
#include <iostream>
#include <cstdio>
#include <cmath>
using namespace std;
const int N = 1e3 + 10;
int n, m;
struct node {
int x, y, r, m;
} infor[N];
int dist(int x, int y) {
int ans = -1;
for (int i = 1; i <= n; i++)
if (abs(infor[i].x - x) + abs(infor[i].y - y) <= infor[i].r) ans = max(ans, infor[i].m);
return ans;
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++)
scanf("%d %d %d %d", &infor[i].x, &infor[i].y, &infor[i].r, &infor[i].m);
while (m--) {
int x, y;
scanf("%d %d", &x, &y);
printf("%d\n", dist(x, y));
}
return 0;
}
E题:天依的聚会 O ( n log n log w ) O(n\log{n}\log{w}) O(nlognlogw)
考察知识:DP,位运算,数据结构
解析:
题意分解,求:1.区间
[
l
,
r
]
[l, r]
[l,r]间所有数按位或的结果
S
(
l
,
r
)
S_{(l,r)}
S(l,r);2.所有
S
(
l
,
r
)
S_{(l,r)}
S(l,r)存在的个数;3.出现最多的
S
(
l
,
r
)
S_{(l,r)}
S(l,r)为多少,以及出现的次数。
这里最难的是哪个?是问题1。没错,万事开头难,要解决问题1还是很有难度的。因为数据范围
n
n
n 已经达到了
1
0
5
10^5
105,明显地,
O
(
n
2
)
O(n^2)
O(n2)的复杂度是不行的。那么解决方法只有一种可能性,那就是DP算法了,很可能是涉及到2进制计算的。在苦思冥想之后,终于我决定,向出题人【黄大师】求助。出题人也是非常地nice,他毫不吝啬地告诉了我这道题的灵感来源——来自力扣898题。所以,嘿嘿,我就去看了那道题的题解,果然是DP。题解的思路确实非常的妙,很难想到。如此问题1就解决了。
力扣898题官方题解
问题2的个数,我们可以用哈希的方式存储下出现的
S
(
l
,
r
)
S_{(l,r)}
S(l,r)的个数,用stl会简单一点;
问题3的次数,我们可以将哈希表内的数字进行排序
解决了3个问题,最后输出前10个最大的
S
(
l
,
r
)
S_{(l,r)}
S(l,r)就行了。
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <unordered_map>
using namespace std;
const int N = 1e5 + 10;
const int M = 32e5 + 10;
int n, a[N], q[M], idx;
int x1[35], x2[35]; //表示值
long long y1[35], y2[35]; //表示次数
int idx1, idx2;
struct node {
int id;
long long sum;
} ans[M];
bool cmp(node a, node b) {
if (a.sum == b.sum) return b.id < a.id;
else return b.sum < a.sum;
}
int main() {
cin >> n;
for (int i = 0; i < n; i++)
scanf("%d", &a[i]);
unordered_map<int, long long> d;
d[a[0]] = 1; q[idx++] = a[0];
x1[0] = a[0], y1[0] = 1, idx1 = 1, idx2 = 0;
for (int i = 1; i < n; i++) {
for (int j = 0; j < idx1; j++) {
int t = a[i] | x1[j];
int k;
for (k = 0; k < idx2; k++)
if (t == x2[k])
break;
if (k == idx2) {x2[idx2] = t; y2[idx2++] = y1[j];}
else y2[k] += y1[j];
}
int k;
for (k = 0; k < idx2; k++)
if (a[i] == x2[k])
break;
if (k == idx2) {x2[idx2] = a[i]; y2[idx2++] = 1;}
else y2[k] += 1;
for (int j = 0; j < idx2; j++)
if (d.count(x2[j])) d[x2[j]] += y2[j];
else {d[x2[j]] = y2[j]; q[idx++] = x2[j];}
for (int j = 0; j < idx2; j++)
x1[j] = x2[j], y1[j] = y2[j];
idx1 = idx2;
idx2 = 0;
}
for (int i = 0; i < idx; i++) ans[i] = {q[i], d[q[i]]};
sort(ans, ans + idx, cmp);
printf("%d\n", idx);
for (int i = 0; i < 10 && i < idx; i++)
printf("%d %lld\n", ans[i].id, ans[i].sum);
return 0;
}
F题: 有校园网的那些日子 O ( n 2 m ) O(n^2m) O(n2m)
考察知识:网络流(最大流)
解析:这道题是求最大流很好的一个模板题。题目中网络的流动其实解释的就是网络流的概念,而题目要我们求的最大带宽也就是最大流的概念。那么最大流怎么求呢?
最大流:很明显地,我们用搜索的方法是可以解决的,求最大流的模板中其实也运用了搜索的方法。但如果我们不用任何技巧地纯暴力搜索的话,会涉及到多次的回溯,复杂度爆炸。那么我们如何优雅地暴力搜素呢?
介绍一种思路:
我们可以每次搜索一条从起点到终点的路径,然后让这条路径上所有边的流量减去路径上所有边流量的最小值,最后将所有可行路径上的流量相加。这样我们是能保证每次搜索到流量是可行的。
样例:
如图,如果第一次搜索找到的路径是S -> A -> T,那么第二次可以搜索到路径 S -> B -> T。我们会得到结果2,明显地我们的正确答案就是2。
但如果我们第一次搜索到的路径是 S -> A -> B -> T, 那么我们会得到结果1,明显1是错误的。那么我们这种思路不可行了吗?我们怎么办呢?
这里先介绍一下网络流里的几条基本性质:
1.容量限制
任意一条边的流量必定小于它的容量。
2.斜对称定理
一条边
(
X
,
Y
)
(X,Y)
(X,Y)中
X
X
X 到
Y
Y
Y的流量必定与其反边
(
Y
,
X
)
(Y,X)
(Y,X)中
Y
Y
Y到
X
X
X的流量相反。
3.流量守恒定理
除了源点
S
S
S 和 汇点
T
T
T外 ,任意节点的流入总量都等于流出总量,即不会存储流量 。
我们可以根据性质2,即斜对称定理,建立可行路径的反边。
建立反边有什么用呢?接着看。
建立反边之后,我们可以发现一条路径S -> B -> A -> T的路径出现了,流量为1。这样我们的答案就正确了。
这里可能会有疑惑了,刚才一条路径A -> B ,现在又一条B -> A呢,可行吗?
方法是可行的,我是这样理解的:首先搜到的路径S -> A -> B -> T,流量为1,可以看作,有1单位的流量流过A,又经B流出;建立反边后搜索到的路径S -> B -> A -> T,流量为1,可以看作,有1单位的流量流过B,又经A流出。合并一下,我们可以看作是
(
A
,
B
)
(A, B)
(A,B)之间没有流量,达到A, B的所有的流量都流向其他节点了。也可以看作后面的路径交换了。
由这样思路所成的算法叫作 Edmons_Karp算法,它的算法复杂度为
O
(
n
m
2
)
O(nm^2)
O(nm2)
以下是Edmons_Karp算法的一个模板:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
const int N = 210, M = 5010;
int n, m, s, t;
long long h[M<<1], e[M<<1], c[M<<1], ne[M<<1], idx;//建边
long long incf[N], st[N], pre[N], maxflow;//流经的流量;是否流经该点,流量的来源,最大流
void add(int x, int y, int z) {
e[idx] = y;
c[idx] = z;
ne[idx] = h[x];
h[x] = idx++;
}
bool EK() { // bfs搜索路径
memset(st, 0, sizeof(st));
queue<int> q;
q.push(s); st[s] = 1;
incf[s] = (long long)1 << 31;
while (q.size()) {
int u = q.front(); q.pop();
for (int i = h[u]; i != -1; i = ne[i])
if (c[i]) {
int v = e[i];
if (st[v]) continue;
incf[v] = min(incf[u], c[i]);
pre[v] = i;
q.push(v), st[v] = 1;
if (v == t) return 1;
}
}
return 0;
}
void update () { //反向更新可行路径及建立反边
int u = t;
while (u != s) {
int v = pre[u];
c[v] -= incf[t];
c[v^1] += incf[t];
u = e[v^1];
}
maxflow += incf[t];
}
int main() {
cin >> n >> m >> s >> t;
memset(h, -1, sizeof(h));
for (int i = 0; i < m; i++) {
int x, y, z;
scanf("%d %d %d", &x, &y, &z);
add(x, y, z);
add(y, x, 0);
}
while (EK()) update();
cout << maxflow << endl;
return 0;
}
EK算法的复杂度太高了,所以在优化后出现了Dinic算法。
Dinic算法运用二分图的知识对搜索的过程进行了剪枝优化,对有后续流量不可能达到终点的节点进行剪枝,大大优化了算法复杂度,使之能达到
O
(
n
2
m
)
O(n^2m)
O(n2m)的复杂度,而这只是理论上的复杂度,事实上的复杂度会远远小于
O
(
n
2
m
)
O(n^2m)
O(n2m)。
优化的思路:
由于我们bfs搜索时,搜索到某个节点的路径都是最短路,所以可知各个节点是有层级顺序的,所以我们可以将各节点的层级求出,然后用dfs进行搜索。在有层级网络流中进行dfs搜索一条路径明显会优于bfs,因为有一些层级不符合的边我们可以省略,而且在搜索的过程中还可以进行剪枝。
Dinic算法在搜索路径前会先计算个节点的层级,还会判断是否还有可行路径。所以我们只要在求完最大流之后,对某条边的容量进行扩大,看扩大后是否会出现新的路径,就能判断某条边扩容后会不会影响最大流。而且这个计算不会影响残余网络,所以Dinic算法用在F题非常恰当,这也是为什么说这是一道很好的模板题的原因。
以下是F题的Dinic算法的代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
const int INF = 1024;
const int N = 510;
const int M = 5010;
using namespace std;
int n, m, s, t, maxflow;
int w[M][3]; // 因为后面要判断路径容量改变后是否会影响最大流,所以这里要把每条边存起来。
int h[M<<1], e[M<<1], c[M<<1], ne[M<<1], idx; //链式前向星进行建边
int d[N];//节点的层级
queue<int> q;
void add(int x, int y, int z) {
e[idx] = y;
c[idx] = z;
ne[idx] = h[x];
h[x] = idx++;
}
bool bfs() {//每次寻找路径时都要重新计算层级,并判断是否还有可行路径
memset(d, 0, sizeof(d));
while (q.size()) q.pop();
q.push(s); d[s] = 1;
while (q.size()) {
int x = q.front(); q.pop();
for (int i = h[x]; i != -1; i = ne[i])
if (c[i] && !d[e[i]]) {
q.push(e[i]);
d[e[i]] = d[x] + 1;
if (e[i] == t) return 1;
}
}
return 0;
}
int dinic(int x, int flow) {
if (x == t) return flow;
int rest = flow, k;
for (int i = h[x]; i != -1 && rest; i = ne[i])
if (c[i] && d[e[i]] == d[x] + 1) {
k = dinic(e[i], min(rest, c[i]));
if (!k) d[e[i]] = 0;
c[i] -= k;
c[i^1] += k;
rest -= k;
}
return flow - rest;
}
int main() {
cin >> n >> m;
s = 1, t = n;
memset(h, -1, sizeof(h));
for (int i = 0; i < m; i++) {
scanf("%d %d %d", &w[i][0], &w[i][1], &w[i][2]);
add(w[i][0], w[i][1], w[i][2]);
add(w[i][1], w[i][0], 0);
}
int flow = 0;
while (bfs())
while (flow = dinic(s, INF)) maxflow += flow;
if (maxflow) cout << maxflow << endl;
else printf("I waste thirty yuan each month!\n");
int ans = 0;
for (int i = 0; i < m; i++) {
c[i*2]++;
if (bfs()) ans++;
c[i*2]--;
}
cout << ans << endl;
return 0;
}