【题解】2019太原理工大学第二届程序设计新生赛预赛暨公开赛题解
A - Creeper?
按照题目要求,判断输入的字符串是什么,输出对应的字符串即可。
读入时需要注意一次读入一行,因为"Awww man."和"Se no!"中间有空格。
C++:
#include <bits/stdc++.h> using namespace std; char str[15]; int main(void) { int n; scanf("%d", &n); fgets(str, 15, stdin); while (n--) { fgets(str, 15, stdin); if (str[0] == 'C') printf("Awww man.\n"); else if (str[0] == 'A') printf("Se no!\n"); else if (str[0] == 'S') printf("Creeper?\n"); } }
Python 3:
n = int(input()) for i in range(0, n): s = input() if s == "Creeper?": print("awwwman.") elif s == "Awww man.": print("seno!") else: print("creeper?")
Java:
import java.util.Scanner; public class creeper { public static void main(String[] args) { Scanner in = new Scanner(System.in); int n; n = in.nextInt(); in.nextLine(); for (int i = 0; i < n; i++) { String str = in.nextLine(); switch (str.charAt(0)) { case 'C': System.out.print("awwwman."); break; case 'A': System.out.print("seno!"); break; case 'S': System.out.print("creeper?"); break; } } } }
B - Awww Man
第一个观察,最终得分来源于两部分,分别是吃豆子得到的分数和移动所损失的分数。
因为要求吃完所有豆子,而每个豆子的得分是固定的,于是这一部分的分数是固定的,即所有豆子的分数之和。
而对于第二部分——移动的分数取决于移动的距离,或者说是吃豆子的策略。
第二个观察,想要让移动损失得分尽可能小,显然要减少移动距离,并选择最合适的发动能力的时机。
第三个观察,将发动能力前后的游戏分开来看,发动能力前的移动路径不存在优化可能,因为豆子是一个接着一个出现的,只有吃完前一个豆子才可以吃下一个,于是在此条件下,最短移动路径就是按照豆子挨个出现的位置来走的。
第四个观察,如果只考虑发动能力之后的局面,可以发现此时问题变得很像我们电梯调度、磁盘调度的问题。假设此状态下最左侧的豆子在位置,最右侧的豆子在位置,则这个问题变成了从某个位置开始,把上的所有豆子都吃掉的最短移动路径,由于这个区间两端都有豆子,这等价于求从位置把区间上的所有位置都经过一遍,如果同属的一侧(即都小于等于或都大于等于),则显然从位置直接走过去就是最短路径了;相对复杂的情形是,当在的两侧时,需要决策是先走到再返回去走,还是先去再去,这个距离只需要求解二者最小值即可,即,仔细观察可以发现,这一情况下的移动,共性的部分是把区间从头到尾走了一遍,上面的表达式可写作。
第五个观察,决策何时发动能力。简单思考,即可发现,这个问题是希望寻找一个时刻,此时已经吃掉了颗豆子,剩下颗豆子还没吃,发动能力,相当于把颗豆子分成两部分,前一部分用第三个观察中的方法来计算代价,后一部分用第四个观察中的方法来计算代价。对于前颗豆子的移动代价,这个值可以记作,可以发现这是给定的长度为的位置序列的一阶差分序列的绝对值的前缀和序列。对于剩下的个豆子,只需要知道这些豆子位置的最小值和最大值,这显然是序列的后缀最小值和后缀最大值,于是可以先预处理前面所说的代价前缀和,之后倒序遍历序列,一边维护后缀最小位置和最大位置,一边尝试更新答案,这个方法可以用的时间复杂度完成答案的最优化。
第六个观察,当决定在吃完某个豆子后发动能力之前应当做什么。上面的方法有一个细节,便是用什么来尝试更新答案。因为题目说明在发动能力前单位移动代价为,而发动后为,显然希望在发动能力前后两部分交接的时候,尽可能将更多的移动距离放在发动能力之前解决(因为这样单位移动代价小),这可以理解为是为了使发动能力后移动距离尽可能小所做的准备工作,对于发动能力后的部分,实际上有效的吃豆子范围是(即后缀最小位置到后缀最大位置),其它范围上没有豆子,因此可以在发动技能之前先以单位移动代价移动到其中一个端点上,之后发动能力并以单位移动代价把这个区间上的所有豆子吃完。于是倒序遍历的过程中每次计算
,即是移动开销。
为什么是这样?不妨假设位置和上的豆子都还没有出现,此时如果移动到了某一个端点,为了吃掉发动技能后才在某个端点出现的豆子,就必须立刻发动能力,否则走过了就需要再以二倍移动代价走回来吃这个豆子,一前一后会比在端点发动能力多了段三倍移动代价的路径,这显然不会比在端点发动更优。如果某个端点上的豆子刚好是吃完此时所假设的发动能力前吃的豆子后出现的豆子,走到这里会立刻吃掉这个豆子,可能会造成后缀区间范围变小,但是这个并不需要考虑,因为再吃一个豆子的情况不属于当前假设下吃掉发动能力前最后一个豆子的考虑范围之内,并且可以发现这样并不会漏解,它被算到发动能力前再多吃一个豆子的情形之内了。
Time Complexity: O(n)
C++:
#include <bits/stdc++.h> using namespace std; typedef long long ll; char buf[80000000], *bcur = buf; inline int rd() { int f = 1, ret = 0; while (*bcur == ' ' || *bcur == '\n' || *bcur == '\t') ++bcur; if (*bcur == '-') f = -1, ++bcur; while (*bcur >= '0' && *bcur <= '9') ret = (ret << 3) + (ret << 1) + *bcur++ - '0'; return f * ret; } char ob[100], *oc = ob; inline void pt(ll val) { if (val < 0) *oc++ = '-', val = -val; static char tmp[25]; int pos = 0; if (val) { while (val) { tmp[pos++] = val % 10 + '0'; val /= 10; } } else tmp[pos++] = '0'; while (pos--) *oc++ = tmp[pos]; } inline void fl() { fwrite(ob, 1, oc - ob, stdout); } const int lim = 1e5 + 10; const ll inf = 1000000000000000005ll; ll a[lim], v[lim], c, pre[lim]; int n; int main(void) { fread(buf, 1, 80000000, stdin); ll ans = -inf, sum = 0ll; n = rd(); c = rd(); a[0] = rd(); for (int i = 1; i <= n; ++i) a[i] = rd(), sum += (v[i] = rd()); for (int i = 1; i <= n; ++i) pre[i] = pre[i - 1] + std::abs(a[i] - a[i - 1]) * c; ans = std::max(ans, sum - pre[n]); ll mini = a[n], mxi = a[n]; for (int i = n; i >= 1; --i) { ans = std::max(ans, sum - pre[i - 1] - 2ll * c * (mxi - mini) - c * std::min(std::abs(mxi - a[i - 1]), std::abs(a[i - 1] - mini))); mini = std::min(mini, a[i - 1]), mxi = std::max(mxi, a[i - 1]); } pt(ans); fl(); return 0; }
Python 3:
tmp = input().split(" ") n = int(tmp[0]) c = int(tmp[1]) sum = 0 v = [0] a = [] pre = [0] tmp = int(input()) a.append(tmp) for i in range(0, n): tmp = input().split(" ") a.append(int(tmp[0])) v.append(int(tmp[1])) sum += v[i + 1] pre.append(pre[i] + c * abs(a[i + 1] - a[i])) ans = sum - pre[n] mxi = a[n] mni = a[n] for i in range(n, 0, -1): ans = max(ans, sum - pre[i - 1] - 2 * c * (mxi - mni) - c * min(abs(a[i - 1] - mni), abs(a[i - 1] - mxi))) mni = min(mni, a[i - 1]) mxi = max(mxi, a[i - 1]) print(ans)
Java:
import java.util.Scanner; import java.util.TreeSet; public class awww_man_java { private static long dist(long a, long b, long c) { if (a <= b) return c - a; if (a >= c) return a - b; return Math.min(c - a + c - b, a - b + c - b); } public static void main(String[] args) { Scanner in = new Scanner(System.in); int n; long C; n = in.nextInt(); C = in.nextLong(); long a[] = new long[n + 1]; long v[] = new long[n + 1]; long sum = 0; for (int i = 0; i <= n; i++) { a[i] = in.nextLong(); if (i != 0) { v[i] = in.nextLong(); sum += v[i]; } } long pre[] = new long[n + 1]; for (int i = 1; i <= n; i++) { pre[i] = pre[i - 1] + Math.abs(a[i] - a[i - 1]) * C; } long ans = sum - pre[n]; long mni = a[n], mxi = a[n]; for (int i = n; i >= 1; i--) { ans = Math.max(ans, sum - pre[i - 1] - 2 * C * (mxi - mni) - C * Math.min(Math.abs(a[i - 1] - mni), Math.abs(a[i - 1] - mxi))); mni = Math.min(mni, a[i - 1]); mxi = Math.max(mxi, a[i - 1]); } System.out.println(ans); } }
C - So We Back In The Mine
按照题目所给定的数据范围,显然每次暴力查询会超时,预处理打表也会发现这个表很难通过数组存下来。
接下来考虑数学方法。
为了使问题变得简单,不如先考虑查询区域横纵坐标都大于等于的情况。将每个格子上的钻石数写下来画一张图,可以发现这张图看起来很像洋葱(误)。看到富有规则的洋葱形排列,这使你充满了决心,如果能把区域和值的问题换成从到某个位置这个区域进行求和的问题,那么这道题将能被很轻松地解决,而且确实换得了。
如下图所示:
假设求解蓝***域的和值,等价于求取上图四个区域的和值,减去(绿色+灰***域的和值,减去(红色+灰***域的和值,加上灰***域的和值,就得到了蓝***域的和值,可以发现上述四块区域都变成了以为左下角坐标的区域,即二维前缀和方法。
现在问题便是如何求解区域 ~ 的和值(前者为左下角坐标,后者为右上角坐标),如果,即区域 ~ 的值,可以发现这个正方形区域和值就是:,可以通过的时间复杂度预处理,但是这里主要描述时间复杂度的数学方法,我们按照与之前类似的思路,对这一正方形区域进行拆分,首先考虑对角线上元素之和:,这个就是自然数等差数列和;之后考虑包含对角线与下三角元素之和:,这个东西是自然数平方序列和,于是这个矩形的和等于倍自然数平方序列和减去多余的一个自然数等差数列和。接下来考虑的情形,此情形下,如果令,则区域 ~ 按照上述方法即可求和,若令,则剩余部分的和为等差数列的倍数:,将两部分相加即可。
按照上述方法,可以求出第一象限内所有区域的和值,接下来思考四个象限时的情况,根据区域所占据的象限情况可以分成九类讨论,没一类都按照象限进行剖分,根据对称性,非第一象限的部分可以转换到第一象限求解,九类情形剖分求解后求和即可。
Time Complexity: 单次询问,总计。
C++:
#include <bits/stdc++.h> using namespace std; typedef long long ll; inline int sgn(ll x) { if (!x) return 0; return x < 0 ? -1 : 1; } int q; ll x1, y1, x2, y2, ans; int sx1, sy1, sx2, sy2; inline ll calc(ll x, ll y) { ll mini = std::min(x, y) + 1ll, maxi = std::max(x, y) + 1ll; return mini * (mini + 1ll) * (2ll * mini + 1ll) / 3ll - mini * (1ll + mini) / 2ll + mini * (maxi - mini) * (maxi + mini + 1) / 2ll; } inline ll g(ll x1, ll y1, ll x2, ll y2) { x1 = std::abs(x1), y1 = std::abs(y1), x2 = std::abs(x2), y2 = std::abs(y2); ll ans = calc(x2, y2); if (sx1 && sy1) ans = ans - calc(x1 - 1, y2) - calc(x2, y1 - 1) + calc(x1 - 1, y1 - 1); else if (sx1) ans = ans - calc(x1 - 1, y2); else if (sy1) ans = ans - calc(x2, y1 - 1); return ans; } int main(void) { scanf("%d", &q); while (q--) { scanf("%lld%lld%lld%lld", &x1, &y1, &x2, &y2); sx1 = sgn(x1), sy1 = sgn(y1), sx2 = sgn(x2), sy2 = sgn(y2); if (sx1 >= 0 && sy1 >= 0) ans = g(x1, y1, x2, y2); else if (sx2 <= 0 && sy2 <= 0) ans = g(x2, y2, x1, y1); else if (sx1 <= 0 && sx2 <= 0 && sy1 >= 0 && sy2 >= 0) ans = g(x2, y1, x1, y2); else if (sx1 >= 0 && sx2 >= 0 && sy1 <= 0 && sy2 <= 0) ans = g(x1, y2, x2, y1); else if (sx1 <= 0 && sx2 >= 0 && sy1 >= 0 && sy2 >= 0) ans = g(1ll, y1, x2, y2) + g(0ll, y1, x1, y2); else if (sx1 <= 0 && sx2 <= 0 && sy1 <= 0 && sy2 >= 0) ans = g(x2, 1ll, x1, y2) + g(x2, 0ll, x1, y1); else if (sx1 <= 0 && sx2 >= 0 && sy1 <= 0 && sy2 <= 0) ans = g(1ll, y2, x2, y1) + g(0ll, y2, x1, y1); else if (sx1 >= 0 && sx2 >= 0 && sy1 <= 0 && sy2 >= 0) ans = g(x1, 1ll, x2, y2) + g(x1, 0ll, x2, y1); else if (sx1 <= 0 && sx2 >= 0 && sy1 <= 0 && sy2 >= 0) ans = g(1ll, 1ll, x2, y2) + g(0ll, 1ll, x1, y2) + g(1ll, 0ll, x2, y1) + g(0ll, 0ll, x1, y1); printf("%lld\n", ans); } return 0; }
Java:
import java.util.*; import java.io.*; public class so_we_back_in_the_mine_fast { private static int q; private static long x1, y1, x2, y2, ans; private static int sx1, sy1, sx2, sy2; private static long calc(long x, long y) { long mini = Math.min(x, y) + 1; long maxi = Math.max(x, y) + 1; return mini * (mini + 1) * (2 * mini + 1) / 3 - mini * (1 + mini) / 2 + mini * (maxi - mini) * (maxi + mini + 1) / 2; } private static long g(long x1, long y1, long x2, long y2) { x1 = Math.abs(x1); y1 = Math.abs(y1); x2 = Math.abs(x2); y2 = Math.abs(y2); long ans = calc(x2, y2); if (sx1 != 0 && sy1 != 0) ans = ans - calc(x1 - 1, y2) - calc(x2, y1 - 1) + calc(x1 - 1, y1 - 1); else if (sx1 != 0) ans = ans - calc(x1 - 1, y2); else if (sy1 != 0) ans = ans - calc(x2, y1 - 1); return ans; } private static int sgn(long x) { if (x == 0) return 0; return x < 0 ? -1 : 1; } public static void main(String[] args) throws IOException { IO in = new IO(); q = in.nextInt(); for (int w = 0; w < q; w++) { x1 = in.nextLong(); y1 = in.nextLong(); x2 = in.nextLong(); y2 = in.nextLong(); sx1 = sgn(x1); sy1 = sgn(y1); sx2 = sgn(x2); sy2 = sgn(y2); if (sx1 >= 0 && sy1 >= 0) ans = g(x1, y1, x2, y2); else if (sx2 <= 0 && sy2 <= 0) ans = g(x2, y2, x1, y1); else if (sx1 <= 0 && sx2 <= 0 && sy1 >= 0 && sy2 >= 0) ans = g(x2, y1, x1, y2); else if (sx1 >= 0 && sx2 >= 0 && sy1 <= 0 && sy2 <= 0) ans = g(x1, y2, x2, y1); else if (sx1 <= 0 && sx2 >= 0 && sy1 >= 0 && sy2 >= 0) ans = g(1, y1, x2, y2) + g(0, y1, x1, y2); else if (sx1 <= 0 && sx2 <= 0 && sy1 <= 0 && sy2 >= 0) ans = g(x2, 1, x1, y2) + g(x2, 0, x1, y1); else if (sx1 <= 0 && sx2 >= 0 && sy1 <= 0 && sy2 <= 0) ans = g(1, y2, x2, y1) + g(0, y2, x1, y1); else if (sx1 >= 0 && sx2 >= 0 && sy1 <= 0 && sy2 >= 0) ans = g(x1, 1, x2, y2) + g(x1, 0, x2, y1); else if (sx1 <= 0 && sx2 >= 0 && sy1 <= 0 && sy2 >= 0) ans = g(1, 1, x2, y2) + g(0, 1, x1, y2) + g(1, 0, x2, y1) + g(0, 0, x1, y1); System.out.println(ans); } } static class IO { final private int LIM = 1 << 16; private DataInputStream in; private byte[] buf; private int ptr, btr; public IO() { in = new DataInputStream(System.in); buf = new byte[LIM]; ptr = btr = 0; } private void flush() throws IOException { btr = in.read(buf, ptr = 0, LIM); if (btr == -1) buf[0] = -1; } public byte gc() throws IOException { if (btr == ptr) flush(); return buf[ptr++]; } public void close() throws IOException { if (in != null) in.close(); } public int nextInt() throws IOException { int ret = 0, f = 1; byte ch; while ((ch = gc()) <= ' '); if (ch == '-') { f = -1; ch = gc(); } while (ch >= '0' && ch <= '9') { ret = (ret << 3) + (ret << 1) + ch - '0'; ch = gc(); } return f * ret; } public long nextLong() throws IOException { long ret = 0, f = 1; byte ch; while ((ch = gc()) <= ' '); if (ch == '-') { f = -1L; ch = gc(); } while (ch >= '0' && ch <= '9') { ret = (ret << 3L) + (ret << 1L) + ch - '0'; ch = gc(); } return f * ret; } } }
备注:出这道题的时候并行开多线程暴力验证了一下正确性用的程序,虽然OJ不支持,但是本地跑或者找个计算服务器跑还是很快的。
OpenMP Prallel version:
#include <bits/stdc++.h> using namespace std; typedef long long ll; int main(void) { int q; scanf("%d", &q); ll x1, y1, x2, y2; while (q--) { scanf("%lld%lld%lld%lld", &x1, &y1, &x2, &y2); ll ans = 0ll; #pragma omp parallel for reduction(+:ans) for (ll i = x1; i <= x2; i++) for (ll j = y1; j <= y2; j++) ans += std::max(std::abs(i), std::abs(j)) + 1; printf("%lld\n", ans); } return 0; }
D - Imperishable Night
经典题,差分序列维护更新,最后求前缀和还原的经典题目。
如果在原序列上修改,需要修改这个区间的所有元素,而如果在一阶差分序列上修改,只需要修改端点即可,单次修改做到时间复杂度,最后时间复杂度还原。
Time Complexity:
C++:
#include <bits/stdc++.h> using namespace std; const int lim = 1e6 + 10; int dat[lim]; int main(void) { int n, q, l, r, k; scanf("%d%d", &n, &q); while (q--) { scanf("%d%d%d", &l, &r, &k); dat[l] += k, dat[r + 1] -= k; } for (int i = 1; i <= n; i++) { if (i != 1) putchar(' '); dat[i] += dat[i - 1]; printf("%d", dat[i]); } return 0; }
E - Mysterious Mountain
经典题,序列第三大的数。
数据量不大,三种做法。
方法一:排序,如果使用快速排序或STL sort使用的内省排序,可以做到时间复杂度,因为序列值域只有,使用桶排序可以做到时间复杂度。
方法二:使用桶计数,然后倒过来找,看到哪个数时桶的后缀和大于等于,时间复杂度。
方法三:类似于擂台策略,属于经典做法,建立三个变量,存储维护最大值、次大值、第三大值,遍历一遍序列即可,可以做到时间复杂度与空间复杂度。
另外,通用的第大/小方法,如主席树、整体二分同样可以解决问题。
C++:
#include <bits/stdc++.h> using namespace std; int main(void) { int n, a, b, c, x; a = b = c = 0; scanf("%d", &n); while (n--) { scanf("%d", &x); if (x > a) { c = b; b = a; a = x; } else if (x > b) { c = b; b = x; } else if (x > c) { c = x; } } printf("%d", c); return 0; }
F - Cirno's Perfect Algorithm Class
经典题,相邻交换排序最小交换次数。
为了在寒冷的隆冬能够给广大朋友送去温暖,于是这道题在命题人与验题人的不断阻拦下,无论是序列长度还是最值大小都被砍成了不超过,如此小的范围几乎没有什么做法是不可取的(前提是做法正确)。
最简单的方法,可以一边冒泡排序,一边计数,时间复杂度。
如果稍作观察,可以发现每次交换最多减少个逆序对,于是可以证明,最小交换次数等于逆序对数。
在这个数据范围下,求解逆序对有多种办法,一种是在数据量扩大一些时也能使用的借助Fenwick Tree(树状数组, Binary Indexed Tree)统计逆序对方法,时间复杂度,使用归并求取逆序对同样具有时间复杂度,另外,因为所有数值的取值范围小,完全可以在遍历的同时开一个计数数组记录遍历到当前位置为止所有数各自出现几次,然后暴力枚举每个比它大/小(取决于遍历顺序)的数统计一共出现几次,即暴力求取逆序对,时间复杂度。
Time Complexity: / / .
C++:
#include <bits/stdc++.h> using namespace std; const int lim = 110; int dat[lim]; int main(void) { int n, cc = 0; scanf("%d", &n); for (int i = 0; i < n; i++) scanf("%d", dat + i); for (int i = n - 1; i >= 0; i--) for (int j = 0; j < i; ++j) if (dat[j] > dat[j + 1]) std::swap(dat[j], dat[j + 1]), ++cc; printf("%d", cc); return 0; }
G - Graze
模拟/实现。
标题名称是擦弹,是某著名STG游戏的一种通过靠近子弹来获得分数的机制,不过与这道题目并没什么关系。。
这道题目做法还是蛮多的,要判断对齐坐标轴的矩形交,使用类似于线段的快速排斥试验即可完成,这样做唯一的麻烦就是需要把自机的判定区由中心、高半径、宽半径表示形式转化为左下角、右上角的表示形式。
另一种办法是,考虑把碰撞箱的表示形式转换为中心、高半径、宽半径的表示形式,不过这样因为有潜在的浮点风险,于是在写判定公式的时候预先在两侧乘二,使得所有数始终落在整数域,判断方式就是中心的水平距离小于等于款半径之和以及中心的竖直距离小于等于高半径之和。
所有操作都可以在时间复杂度完成,于是总体时间复杂度为。
Time Complexity: .
C++:
#include <bits/stdc++.h> using namespace std; inline bool judge(int a, int b, int c, int d, int e, int f, int g, int h) { return std::abs(a - e) <= c + g && std::abs(b - f) <= d + h; } int main(void) { int x, y, w, h, a, b, c, d, op, q; scanf("%d%d%d%d", &x, &y, &w, &h); scanf("%d", &q); while (q--) { scanf("%d", &op); if (op != 3) scanf("%d%d", &a, &b); else scanf("%d%d%d%d", &a, &b, &c, &d); switch (op) { case 1: putchar(x != a || y != b ? '2' : '3'); x = a, y = b; break; case 2: putchar(judge(x, y, w, h, a, b, 0, 0) ? '1' : '0'); break; case 3: putchar(judge(2 * x, 2 * y, 2 * w, 2 * h, a + c, b + d, c - a, d - b) ? '1' : '0'); break; } } return 0; }
H- History of The Stars
简单组合数学,没什么前置知识,只需要知道二项式定理和乘法原理这两个高中知识即可解决。
首先只考虑一个颜色,其方案数等于这种颜色任抽个的方案数之和,根据二项式定理,如果有个,则方案数为。
两种颜色无关,根据乘法原理,总方案数应当是,于是只需要寻找最小的使大于等于星星个数即可。
Time Complexity: .
C++:
#include <bits/stdc++.h> using namespace std; typedef long long ll; int main(void) { ll n; scanf("%lld", &n); ll tmp = 4ll; int cc = 1; while (n > tmp) tmp *= 4ll, ++cc; printf("%d", cc); return 0; }
I - Immortal Grass
经典题。连通块大小。
bfs、dfs、并查集都是可以解的,使用并查集注意路径压缩。
C++: (BFS version)
#include <bits/stdc++.h> #define pb push_back using namespace std; const int lim = 1e5 + 10; vector<int> adj[lim]; bool vis[lim]; int main(void) { int n, m, k, u, v; scanf("%d%d%d", &n, &m, &k); for (int i = 0; i < m; ++i) { scanf("%d%d", &u, &v); adj[u].pb(v), adj[v].pb(u); } queue<int> que; int cc = 1; vis[k] = 1; que.push(k); while (!que.empty()) { int c = que.front(); que.pop(); for (int i = 0; i < adj[c].size(); ++i) { if (!vis[adj[c][i]]) que.push(adj[c][i]), vis[adj[c][i]] = true, ++cc; } } printf("%d", cc); return 0; }
C++: (Union-Find Set version)
#include <bits/stdc++.h> using namespace std; const int lim = 1e5 + 10; int fa[lim]; int Find(int x) { if (x != fa[x]) return fa[x] = Find(fa[x]); return x; } int main(void) { int u, v, n, m, k; scanf("%d%d%d", &n, &m, &k); for (int i = 1; i <= n; i++) fa[i] = i; while (m--) { scanf("%d%d", &u, &v); int nx = Find(u), ny = Find(v); if (nx != ny) fa[nx] = ny; } int ans = 0; for (int i = 1; i <= n; ++i) ans += Find(fa[i]) == Find(fa[k]); printf("%d", ans); return 0; }
J - Minus K
经典题,静态序列区间和查询。
求取区间的函数值,等价于求取其前缀和函数在端点的差值。
预处理(对于这道题是函数),之后求前缀和,再每次时间复杂度查询即可。
注意取模结果要转成非负数。
Time Complexity: .
C++:
#include <bits/stdc++.h> using namespace std; typedef long long ll; const ll mod = 20090909; const int lim = 1e5 + 10; ll cnt[lim], K[lim]; int main(void) { int n, q, l, r; scanf("%d%d", &n, &q); cnt[0] = 1ll; for (int i = 1; i <= n; ++i) { scanf("%lld", K + i); cnt[i] = (cnt[i - 1] * K[i]) % mod; } for (int i = 1; i <= n; ++i) (cnt[i] += cnt[i - 1]) %= mod; while (q--) { scanf("%d%d", &l, &r); if (l == 0) printf("%lld\n", cnt[r]); else printf("%lld\n", ((cnt[r] - cnt[l - 1]) % mod + mod) % mod); } return 0; }
K - The Map of Diamonds
同样是一道被史诗级削弱过的题,原版有四个操作,即现在的两个操作加上水平、竖直翻转,但是发现那样的话会与这道题难度定位不符,于是只剩下了旋转操作了。
只要不是每次旋转都暴力更新,而是记录相较于最原始位置进行了怎样的旋转,最后输出前更新即可解决。
(因为数据较大,虽然测试的时候没有问题,但是实际比赛的时候,部分选手的实现被卡了常数,十分抱歉)
Time Complexity: .
C++:
#include <bits/stdc++.h> using namespace std; const int lim = 1000 + 10; int dat[lim][lim], tmp[lim][lim], fli[2]; int n, q, op; inline void rote() { for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) tmp[j][n - 1 - i] = dat[i][j]; for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) dat[i][j] = tmp[i][j]; } inline char gc() { static char ret; while ((ret = getchar()) == ' ' || ret == '\n' || ret == '\t'); return ret; } int main(void) { scanf("%d%d", &n, &q); for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) scanf("%d", &dat[i][j]); int tt = 0; while (q--) { op = gc(); if (op == '1') ++tt; else if (op == '2') --tt; } tt = (tt % 4 + 4) % 4; for (int i = 0; i < tt; ++i) rote(); for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { printf("%d ", dat[i][j]); } putchar('\n'); } return 0; }
L - Mars Automaton
三种操作重新分类整理,可看做:区间赋值、区间求和、区间非零染色数三种操作,看起来十分复杂,但是不要怕,消除恐惧的最好办法就是出题人说数据均匀随机。
因为数据均匀随机,所以很多极端情况的发生概率变得很低,即便发生,次数也不会太多,但是因为这样随机选取子区间的期望长度为,接近,如果暴力实现每次操作对序列本身进行维护仍然会有的时间复杂度而导致超时。
于是反向思考,不如考虑维护“颜色”本身(即每种数),对于每种数而言,再考虑维护其占据着哪段序列。初始情况下,显然全部都是,整个序列此时是一整块,于是只需使用二元组并将其记录在数值为这一种类下,表示这一整块都是。
有了这个想法,我们需要返回去确认一点,那就是整段序列上数的种类数会有多少,要解决这个问题,我们先思考一下新的种类的数是如何产生的,首先正如之前所说的,开始时一整块都是,此时数的种类数为;之后,有的概率执行操作一使得种类数无改变,有的概率执行操作二,使得种类变成、和另外一个数;有的概率执行操作三,由于只是查询,所以同样不改变结果,也就是说,从只有种数变到有种数,只有的概率。接下来思考之后的变化,如果操作一将之前产生的其它的数包括进去,那个种类在这个区间内的数会直接被覆盖成,相当于会减少种类数;如果操作二将之前产生的一个其它数包括进区间,则那个数可能会被修改为别的数,相当于种类数减一后加一而不变;操作三仍然是查询不会对种类数产生影响。这便是说明了,只有当我们在不覆盖之前产生的新的种类的数的区间上执行操作二才会使种类数增多,在均匀随机的情况下,连续随机选取这样的区间、并且还要恰好是操作二,这个事件发生的概率并不会很高,如果出现了操作一区间包括了之前产生的新数的情况,种类数可能还会减少。具体概率的证明并不容易,这里我们使用蒙特卡罗方法进行估算,将序列长度设置为,每次执行次随机操作,之后记录其全序列上的种类数(包括数字),试验次,全序列种类数出现次数绘制图表如下:
种类数确实不会很多,对于这种元素不是很多,但是有需要动态插入和删除的集合,可以使用链表进行维护。
采用同样的方法进行次试验,得出相同颜色的连通块数量情况:
于是可以更加确定下面的做法,
为了方便描述,定义:
区间块:使用二元组表示,含义是序列上位置上的所有数相同,并且属于这一个整块。
染色块:使用一个数表示,其代表了这个种类的数,是其对应的区间块链的头结点。
区间块链:通过链表按照区间序号从小到大的顺序连接的区间块。
染色块链:通过链表将序列中当前存在的染色块链接起来。
初始状态下,染色块只有一个(即的染色块),它链接的区间块链也只有一个结点(即这个区间块)。
将题目要求的三种操作进行合理的拆分为以下几种操作:
删除区间:删除的结果是使得所有的区间块中不包含这个区间。对于在这个区间内部的区间块,直接删除;对于同时包含了属于这个区间的部分和不属于这个区间的部分的区间块,要进行分裂, 分裂成属于这个区间的区间块和不属于这个区间的区间块,如某个区间块二元组是,其中,则这个区间块会分裂成两个区间块和,之后将属于删除区间的区间块从链表中移除。
添加区间到值为的染色块的区间块链:直接新建一个结点,并记录其代表的是区间块,之后链接到染色块的区间块链中,并仍旧保持顺序即可。如果值为的染色块不存在,则要先新建值为的染色块,并链接到染色块链中。
区间求和操作:暴力每个区间块,将属于这段区间内的数求和并返回。不修改链表结构。因为链在染色块下的所有区间块不会对结果造成影响,直接跳过就可以其整条区间块链即可。
区间染色查询操作:暴力每个非的染色块,判断其是否有区间块与这个区间相交,有则对结果加一。
碎片合并操作:在不断的操作的过程中,很多完整的块可能被分裂成一个个碎片,这些碎片会增大遍历时的时间开销,于是需要把相邻的区间块合并。根据此题的操作情况,只有和染色块的区间块链可能产生未合并的碎片,于是此合并操作只遍历这两种染色块的区间块链即可。
细分成上述几个原子操作后,题目的三个操作分别变成:
操作一:删除操作,添加操作。
操作二:求和操作,删除操作,添加操作,添加操作。(对于的操作二,直接忽略即可)
操作三:染色查询操作。
对于碎片合并操作,如果执行太密集,每次合并的碎片数不多,并且会产生过多的额外 遍历开销;如果执行太松散,每次合并的碎片数虽然会多,但是其他操作遍历时因为碎片造成的开销会变大。假设每次操作后执行一次碎片合并操作,这个参数就是需要调整的地方,可以选择试验+人工调参,也可以通过运行过程中获得的种种信息启发式自动调整参数,但是如果选择后者,要注意如果这个启发式过于复杂,可能反而会成为程序性能的瓶颈。(实际上对于本题的数据量而言,不进行这样的碎片合并操作也是可以在限制时间内通过测试的)
另一个优化则是,如果操作的是区间,因为区间块链是从小到大排列的,当程序在某个区间块链上遇到了在这个区间右侧的区间块时,可以直接结束对此区间块链的遍历,因为之后的区间块一定不会与操作区间相交。
因为这道题目数据均匀随机,并且有区间推平(区间赋值)操作,使用ODT聚聚提出的Chtholly Tree也是可以轻松通过这道题目的,并且代码量比上述解法要少很多。
C++: (Link version)
#include <bits/stdc++.h> using namespace std; const int bufsz = 2e7; char buf[bufsz], *bcur = buf; #define _ac(a, b, c) b* a; \ if (res == nullptr) {a = (b*)bcur; bcur += (c) * sizeof(b);} \ else { \ a = res; \ res = res->nxt; \ a->set(); \ } struct Node { int l, r; Node *lk, *nxt; void set(int nl = 0, int nr = 0, Node *nk = nullptr, Node *nn = nullptr) { l = nl, r = nr; lk = nk, nxt = nn; } }; Node *res; Node *rt; inline bool inter(int a, int b, int c, int d) { return (c >= a && d <= b) || (c <= a && d >= a) || (c <= b && d >= b); } inline void del(Node *cur) { cur->set(); if (res == nullptr) res = cur; else cur->nxt = res; } inline void erase(int l, int r) { for (Node *i = rt, *w = nullptr; i;) { for (Node *j = i->nxt, *k = i; j; j = j->nxt) { if (j->l > r) break; if (inter(l, r, j->l, j->r)) { if (j->l < l) { _ac(lft, Node, 1); _ac(rgt, Node, 1); lft->set(j->l, l - 1, nullptr, rgt); rgt->set(l, j->r, nullptr, j->nxt); k->nxt = lft; del(j); j = k = lft; } else { if (j->r > r) { _ac(lft, Node, 1); _ac(rgt, Node, 1); lft->set(j->l, r, nullptr, rgt); rgt->set(r + 1, j->r, nullptr, j->nxt); k->nxt = lft; del(j); j = lft; } k->nxt = j->nxt; del(j); j = k; } } k = j; } if (i->nxt == nullptr) { if (w == nullptr) { rt = i->lk; del(i); i = rt; } else { w->lk = i->lk; del(i); i = w->lk; } } else { w = i; i = i->lk; } } } inline void insert(int l, int r, int val) { for (Node *i = rt; i != nullptr; i = i->lk) { if (i->l == val) { Node *j, *w; for (j = i->nxt, w = i; j; j = j->nxt) { if (j->l > r) { _ac(cur, Node, 1); cur->set(l, r, nullptr, j); w->nxt = cur; return; } w = j; } _ac(cur, Node, 1); cur->set(l, r, nullptr, nullptr); w->nxt = cur; return; } } _ac(ncur, Node, 1); _ac(rcur, Node, 1); ncur->set(l, r, nullptr, nullptr); rcur->set(val, val, rt, ncur); rt = rcur; } inline int sum(int l, int r) { int ret = 0; for (Node *i = rt; i; i = i->lk) { if (!i->l) continue; for (Node *j = i->nxt; j; j = j->nxt) { if (j->l > r) break; if (inter(l, r, j->l, j->r)) ret += i->l * (std::min(j->r, r) - std::max(j->l, l) + 1); } } return ret; } inline int getCol(int l, int r) { int ret = 0; for (Node *i = rt; i; i = i->lk) { if (!i->l) continue; for (Node *j = i->nxt; j; j = j->nxt) { if (j->l > r) break; if (inter(l, r, j->l, j->r)) { ++ret; break; } } } return ret; } inline void show() { cout << "START SHOW:" << endl; for (Node *i = rt; i != nullptr; i = i->lk) { for (Node *j = i->nxt; j != nullptr; j = j->nxt) { cout << i->l << " -> " << "(" << j->l << ", " << j->r << ")" << endl; } } } const int ubound = 1 << 31, dbound = 1; int mlim = 256, mcc = 256, pre = 0; inline void merge() { // int cc = 0; for (Node *i = rt; i != nullptr; i = i->lk) { if (i->l == 1 || i->l == 0) { Node *w = i->nxt; Node *j = w->nxt; while (j) { if (j->l - 1 == w->r) { w->r = j->r; w->nxt = j->nxt; del(j); // ++cc; j = w->nxt; } else { w = j; j = j->nxt; } } } } // if (cc >= pre && mlim < ubound) mlim <<= 1; // else if (cc < pre && mlim > dbound) mlim >>= 1; // pre = cc; mcc = mlim; } int main(void) { int n, q, l, r, op; scanf("%d%d", &n, &q); _ac(tt, Node, 1); tt->set(1, n); _ac(tcur, Node, 1); tcur->set(1, 1, nullptr, tt); rt = tcur; while (q--) { scanf("%d%d%d", &op, &l, &r); if (!mcc--) merge(); switch (op) { case 1: erase(l, r); insert(l, r, 1); break; case 2: if (r > l) { int tmp = sum(l, r); erase(l, r); insert(l, r - 1, 0); insert(r, r, tmp); } break; case 3: printf("%d\n", getCol(l, r)); break; } // show(); } return 0; }
C++: (Chtholly Tree version)
#include <iostream> #include <map> #include <algorithm> #include <vector> using namespace std; typedef long long LL; typedef map<LL, LL>::iterator Ite; typedef unsigned int UI; map<LL, LL> ctt; Ite split(LL p) { Ite ret = --ctt.upper_bound(p); if (ret->first != p) ctt[p] = ret->second, ret++; return ret; } void add(LL l, LL r, LL diff) { Ite s = split(l), e = split(r + 1); while (s != e) (s++)->second += diff; } void assign(LL l, LL r, LL x) { Ite s = split(l), e = split(r + 1); while (s != e) ctt.erase(s++); ctt[l] = x; } vector<LL> buff; LL query(LL l, LL r) { Ite s = split(l), e = split(r + 1); buff.clear(); while (s != e) buff.push_back((--e)->second); sort(buff.begin(), buff.end()); LL ret = 0; int i = 0; for (i = 0; i < buff.size() && !buff[i]; i++); for (; i < buff.size(); ++i) if (i == 0 || buff[i] != buff[i - 1]) ++ret; return ret; } LL sum(LL l, LL r) { Ite s = split(l), e = split(r + 1); LL ret = 0; while (s != e) r = (e--)->first, ret = (ret + (r - e->first) * e->second); return ret; } int main(void) { int n, q, op, l, r; scanf("%d%d", &n, &q); for (int i = n + 1; i >= 1; --i) ctt[i] = 1; while (q--) { scanf("%d%d%d", &op, &l, &r); switch (op) { case 1: assign(l, r, 1ll); break; case 2: if (r > l) { assign(r, r, sum(l, r)); assign(l, r - 1, 0ll); } break; case 3: printf("%lld\n", query(l, r)); break; } } return 0; }
M - Vanis and Weird Message
这也是一道温暖题,因为考虑到并非所有的选手未来都会走上算法竞赛的道路,所以还是希望在题目里加入一些与计算机原理相关的题目的,于是就有了这道题。
主要有两种做法,第一种做法是进制做法,按照大端法输入的话,实际上可以发现顺序变成了高位先输入,之后输入低位这样的模式,这在本质上和以字符串序列读入一个整型数据是完全一致的,只是此时变成了进制数,我们需要把它转化为进制数,然后额外处理一下因为计算机补码表示带来的符号表示问题即可。
第二种做法就是严格实现大端法到小端法的转换,这一方法已经在题目备注中描述的很清楚,故题解中不再详述。
对于经验较为丰富的选手应该能很容易就能看出进制做法,而在较短的时间内通过这道题目。而即便是对此不了解的同学,预计也可以通过备注里的介绍来完成这道题目,只是通过时间会相对晚一些。
C++: (base conversion version)
#include <bits/stdc++.h> using namespace std; int main(void) { int n, x, y; scanf("%d", &n); for (int i = 0; i < n; i++) { scanf("%d", &x); int j = i + 1; if (x == 8) { long long ans = 0ll; for (int k = 0; k < x; ++j, ++k) { scanf("%d", &y); ans = (ans << 8ll) | (y & ((1 << 8) - 1)); } printf("%lld\n", ans); } else { int ans = 0; for (int k = 0; k < x; ++j, ++k) { scanf("%d", &y); ans = (ans << 8) | (y & ((1 << 8) - 1)); } printf("%d\n", ans); } i = j - 1; } return 0; }
C++: (pointer version)
#include <stdio.h> const int lim = 1e6 + 10; char dat[lim]; void sw(char *a, char *b) { char t = *a; *a = *b; *b = t; } int main() { int n, x; scanf("%d", &n); for (int i = 0; i < n; ++i) scanf("%d", &x), dat[i] = (char)x; char *pos = dat; while (pos - dat < n) { if (*pos == 4) { for (char *s = pos + 1, *t = pos + 4; s < t; ++s, --t) sw(s, t); printf("%d\n", *((int *)(++pos))); pos += 4; } else if (*pos == 8) { for (char *s = pos + 1, *t = pos + 8; s < t; ++s, --t) sw(s, t); printf("%lld\n", *((long long int *)(++pos))); pos += 8; } } return 0; }