2022年12月10日广东工业大学揭阳校区新生程序设计竞赛决赛部分ABCEF题题解

注:题解仅为个人理解

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;
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值