[关闭]
@Dmaxiya 2020-08-24T16:43:06.000000Z 字数 11101 阅读 1335

第二届 Hello World 程序设计竞赛题解

Hello_World


A. 舍罕王的反击

题意

给定 ,要求构造一个等比数列,该数列首项为 ,公比为 ,前 项和等于 ,输出最小的满足条件的正整数 以及对应的

数据范围

对于 的数据,

题解

最小的正整数为 ,当 时可以发现,必然有满足条件的

标程

  1. #include <stdio.h>
  2. int x;
  3. int main() {
  4. scanf("%d", &x);
  5. printf("1 %d\n", x);
  6. return 0;
  7. }

B. 小班教学

题意

的所有会员总共想学 种语言,有 个人想学第 种语言,要求对所有会员分班,使得满足每个班的人数相等,且每个班内学习的语言相同。

数据范围

对于 的数据,
对于 的数据,
对于 的数据,,答案不超过 int 能表示的范围

题解

第一种暴力解法是枚举所有可能的每个班级的人数,由班级人数计算需要分班的数量,取最小值,最开始给的数据是可以通过一些 break 优化卡过去的,后来加强了最后三组数据,让这种暴力做法只能得到 的分数。
由于每个班级的人数越多,分班数量越少,而且要求满足每种语言的分班人数都相等,所以每个班的人数都要能整除每一种语言的人数,因此每个分班的最大人数就是这 个数字的最大公约数,求最大公约数用辗转相除法,暴力枚举最大公约数的话,也是无法通过最后三组数据。
最后注意虽然答案在 int 范围内,但是 个数字的和是在 long long 范围的,当然也可以像标程这样来控制计算过程中不爆 int

标程

  1. #include <stdio.h>
  2. int n, ans, Gcd;
  3. int num[1000000 + 100];
  4. int gcd(int a, int b) {
  5. if(b == 0) {
  6. return a;
  7. }
  8. return gcd(b, a % b);
  9. }
  10. int main() {
  11. ans = 0;
  12. scanf("%d", &n);
  13. for(int i = 0; i < n; ++i) {
  14. scanf("%d", &num[i]);
  15. }
  16. Gcd = num[0];
  17. for(int i = 1; i < n; ++i) {
  18. Gcd = gcd(Gcd, num[i]);
  19. }
  20. for(int i = 0; i < n; ++i) {
  21. ans += num[i] / Gcd;
  22. }
  23. printf("%d\n", ans);
  24. return 0;
  25. }

C. 小 Hi 的情书

题意

原先有 个单词,将每个单词按照下面的规则加密:

  1. 对每个单词进行加密
  2. 不改变单词的大小写
  3. 针对每个单词,从第一个字母开始标序号,到最后一个字母,取出所有序号为奇数的字母,连起来作为加密后单词的前半部分。再倒着取出所有序号为偶数的字母,作为单词的后半部分

输入加密后的单词,输出解密后的单词。

数据范围

整篇情书的字母数量之和不大于

题解

用两个下标在单词两端扫描,一个从左往右扫,另一个从右往左扫,来填充原单词,当处于原单词奇数位的时候,取左边的下标对应的字母,当处于原单词偶数位的时候,取右边的下标对应的字母,两个下标不断往中间靠拢,直到填满整个单词。

标程

  1. #include <stdio.h>
  2. #include <string.h>
  3. int n, l, r, len;
  4. char str[5000 + 100], ans[5000 + 100];
  5. int main() {
  6. scanf("%d", &n);
  7. for(int i = 0; i < n; ++i) {
  8. scanf("%s", str);
  9. len = strlen(str);
  10. l = 0;
  11. r = len - 1;
  12. for(int j = 1; j <= len; ++j) {
  13. if(j % 2 == 1) {
  14. ans[j] = str[l];
  15. ++l;
  16. } else {
  17. ans[j] = str[r];
  18. --r;
  19. }
  20. }
  21. ans[len + 1] = '\0';
  22. printf("%s ", ans + 1);
  23. }
  24. printf("\n");
  25. return 0;
  26. }

D. 小 Hi 的

题意

给出 范围内的整数,可以对这 个整数进行任意多次的加减操作,若对某个数字进行加一操作,必须要选择另一个数字做减一操作,要求最后得到的 个数字的乘积最大。

数据范围

对于 的数据,
对于 的数据,

题解

由于对其中一个数字加一就要对另一个数字减一,因此 个数字的总和始终不变,第一种做法是在保持总和不变的情况下暴力递归枚举每一种分配的方式,由于 ,因此这种做法一组数据都通过不了。
考虑只有两个数字的情况,由基本不等式可以得到,要使得乘积最大必须要尽量平均分这两个数字,对其扩展证明可以得到,只要尽量平均分配这 个数字的和,就可以得到最大的乘积。令 ,可知 个数中有 个数字被分配为 ,有 个数被分配为 ,因此答案为:

这种做法可能是大多数人猜结论都可以猜出来的做法。
由于验题人并不会对基本不等式扩展进行严格的证明,因此用了第三种解法。定义 表示前 个数字的和为 时能得到的最大乘积。第 个数字可以取的合法范围为 ,枚举第 个数字可能的取值,可以得到递推式:

初始条件为 ,最后的答案就是 ,也就是前 个数字的和为 时乘积的最大值。

标程

std1.c

  1. #include <stdio.h>
  2. int T, sum, x, mod;
  3. int Pow[20][20];
  4. int main() {
  5. for(int i = 1; i <= 10; ++i) {
  6. Pow[i][0] = 1;
  7. for(int j = 1; j <= 7; ++j) {
  8. Pow[i][j] = Pow[i][j - 1] * i;
  9. }
  10. }
  11. scanf("%d", &T);
  12. while(T--) {
  13. sum = 0;
  14. for(int i = 0; i < 7; ++i) {
  15. scanf("%d", &x);
  16. sum += x;
  17. }
  18. mod = sum % 7;
  19. printf("%d\n", Pow[sum / 7][7 - mod] * Pow[sum / 7 + 1][mod]);
  20. }
  21. return 0;
  22. }

std2.c

  1. #include <stdio.h>
  2. int T, x, sum, up;
  3. int dp[20][100];
  4. int min(int a, int b) {
  5. if(a < b) {
  6. return a;
  7. }
  8. return b;
  9. }
  10. int max(int a, int b) {
  11. if(a > b) {
  12. return a;
  13. }
  14. return b;
  15. }
  16. int main() {
  17. for(int i = 0; i <= 9; ++i) {
  18. dp[1][i] = i;
  19. }
  20. for(int i = 2; i <= 7; ++i) {
  21. for(int j = 0; j <= i * 9; ++j) {
  22. dp[i][j] = 0;
  23. up = min(9, j);
  24. for(int k = 0; k <= up; ++k) {
  25. if(j - k <= (i - 1) * 9) {
  26. dp[i][j] = max(dp[i][j], dp[i - 1][j - k] * k);
  27. }
  28. }
  29. }
  30. }
  31. scanf("%d", &T);
  32. while(T--) {
  33. sum = 0;
  34. for(int i = 0; i < 7; ++i) {
  35. scanf("%d", &x);
  36. sum += x;
  37. }
  38. printf("%d\n", dp[7][sum]);
  39. }
  40. return 0;
  41. }

E. 小 Yi 的照片

题意

给出一个 的矩阵,按要求生成一个新的矩阵:

  1. 对于原矩阵中每一个大小为 为奇数)的子矩阵,若其中心坐标为 ,则新矩阵 处的值等于子矩阵内所有数字的中位数;
  2. 其他数字等于原矩阵内的数字。

给定原矩阵以及 ,求新矩阵。

数据范围

对于 的数据,
对于 的数据, 为奇数

题解

直接按题意暴力模拟可以通过。这题主要是一个排序,如果刚学完 C 语言的话可能只会冒泡或者选择排序,理论上写冒泡与选择排序无法通过第四组数据,但是我们不卡排序的复杂度,加上洛谷的评测机跑得飞快,所以 的排序可以通过所有数据。考虑到会用 sort 与不会用 sort 之间存在一定的不公平,所以我们也给出了库函数中的 sort 快速排序的用法示例,减少了手写排序与 debug 的时间。
由于要求标程必须用 C 语言来写,所以下面给出的做法一种是用选择排序,另一种是用计数的方式找到中位数,第一种理论上无法通过第四组但实际上允许通过,第二种理论复杂度可以通过。计数的方式也就是从 统计每个数字出现的次数,用一个变量 不断往 加,一旦 的值大于等于 ,则当前的数字就是中位数。

标程

std1.c

  1. #include <stdio.h>
  2. int n, m, s, Index, x;
  3. int num[600][600], ans[600][600], tmp[200];
  4. void Sort(int *num, int n) {
  5. for(int i = 0; i < n; ++i) {
  6. Index = i;
  7. for(int j = i; j < n; ++j) {
  8. if(num[j] < num[Index]) {
  9. Index = j;
  10. }
  11. }
  12. x = num[i];
  13. num[i] = num[Index];
  14. num[Index] = x;
  15. }
  16. }
  17. int main() {
  18. scanf("%d%d%d", &n, &m, &s);
  19. for(int i = 0; i < n; ++i) {
  20. for(int j = 0; j < m; ++j) {
  21. scanf("%d", &num[i][j]);
  22. ans[i][j] = num[i][j];
  23. }
  24. }
  25. for(int i = 0; i + s - 1 < n; ++i) {
  26. for(int j = 0; j + s - 1 < m; ++j) {
  27. Index = 0;
  28. for(int r = 0; r < s; ++r) {
  29. for(int c = 0; c < s; ++c) {
  30. tmp[Index++] = num[i + r][j + c];
  31. }
  32. }
  33. Sort(tmp, Index);
  34. ans[i + s / 2][j + s / 2] = tmp[Index / 2];
  35. }
  36. }
  37. for(int i = 0; i < n; ++i) {
  38. for(int j = 0; j < m; ++j) {
  39. printf("%d ", ans[i][j]);
  40. }
  41. printf("\n");
  42. }
  43. return 0;
  44. }

std2.c

  1. #include <stdio.h>
  2. int n, m, s, Index, x;
  3. int num[600][600], ans[600][600], cnt[256], tmp[200];
  4. int findMid(int *num, int n) {
  5. int sum = 0;
  6. for(int i = 0; i < 256; ++i) {
  7. cnt[i] = 0;
  8. }
  9. for(int i = 0; i < n; ++i) {
  10. ++cnt[num[i]];
  11. }
  12. for(int i = 0; i < 256; ++i) {
  13. sum += cnt[i];
  14. if(sum >= n / 2 + 1) {
  15. return i;
  16. }
  17. }
  18. return 255;
  19. }
  20. int main() {
  21. scanf("%d%d%d", &n, &m, &s);
  22. for(int i = 0; i < n; ++i) {
  23. for(int j = 0; j < m; ++j) {
  24. scanf("%d", &num[i][j]);
  25. ans[i][j] = num[i][j];
  26. }
  27. }
  28. for(int i = 0; i + s - 1 < n; ++i) {
  29. for(int j = 0; j + s - 1 < m; ++j) {
  30. Index = 0;
  31. for(int r = 0; r < s; ++r) {
  32. for(int c = 0; c < s; ++c) {
  33. tmp[Index++] = num[i + r][j + c];
  34. }
  35. }
  36. ans[i + s / 2][j + s / 2] = findMid(tmp, Index);
  37. }
  38. }
  39. for(int i = 0; i < n; ++i) {
  40. for(int j = 0; j < m; ++j) {
  41. printf("%d ", ans[i][j]);
  42. }
  43. printf("\n");
  44. }
  45. return 0;
  46. }

F. 星际穿越

题意

计算在二维平面上的点 距离椭圆 上的点的最短距离。

数据范围


题解

由于椭圆在四个象限是对称的,且椭圆上距离 最近的点一定与该点在同一象限,所以可以将所有点都放到第一象限,计算椭圆 上到点 最近的距离,以减少计算量。
这题主要有四种思路:
第一种思路是做一条经过 的直线,要求直线与椭圆的交点在椭圆上的切线与直线垂直,对椭圆求导可以得到切线斜率,最后考虑斜率不存在的特殊情况。由于这种做法比较复杂而且最终目的是求解椭圆上点的坐标,计算量大且不如以下方法简便,因此出题人与验题人都没有尝试这种方法。
第二种思路是设椭圆上离 最近的点的坐标为 ,则 满足椭圆方程,且要使:

的值最小,通过对该公式求导令导数等于 取极值可以发现,这样很难算。由于答案只要求精确到小数点后两位,所以可以通过在 区间内枚举 的值,代入上面的公式取 的最小值,就可以得到答案。
第三种思路:由椭圆方程可以得到参数方程:

我们可以将 等分进行枚举,来计算

的最小值(为了数据的正确性,实际上进行了 等分,得到准确的数据后,我们才发现实际上只要 等分就可以 )。
第四种做法是三分,由于给定任意一个第一象限内的点,椭圆上从 枚举 的过程中, 的值总是先减小后增大,满足三分性质,因此可以进行三分,初始的左右端点为 ,而后不断将区间三分,取两个分割点 值大的那一端缩小区间范围,直到两端距离小于某个值。
最后请用 double 进行计算,用 float 进行计算精度误差很大。

标程

std1.c

  1. #include <stdio.h>
  2. #include <math.h>
  3. int T;
  4. double a, b, xx, yy, ans;
  5. double min(double a, double b) {
  6. if(a < b) {
  7. return a;
  8. }
  9. return b;
  10. }
  11. double cal(double x1, double y1, double x2, double y2) {
  12. return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2);
  13. }
  14. int main() {
  15. scanf("%d", &T);
  16. while(T--) {
  17. ans = 1e10;
  18. scanf("%lf%lf%lf%lf", &a, &b, &xx, &yy);
  19. xx = fabs(xx);
  20. yy = fabs(yy);
  21. for(double x2 = 0; x2 <= a; x2 += 0.00002) {
  22. ans = min(ans, cal(xx, yy, x2, sqrt((1 - x2 * x2 / a / a) * b * b)));
  23. }
  24. printf("%.2lf\n", sqrt(ans));
  25. }
  26. return 0;
  27. }

std2.c

  1. #include <stdio.h>
  2. #include <math.h>
  3. int T;
  4. double a, b, xx,yy, ans;
  5. double min(double a, double b) {
  6. if(a < b) {
  7. return a;
  8. }
  9. return b;
  10. }
  11. double get_dis(double theta) {
  12. double x = a * cos(theta);
  13. double y = b * sin(theta);
  14. return (x - xx) * (x - xx) + (y - yy) * (y - yy);
  15. }
  16. int main() {
  17. double PI = acos(-1.0);
  18. scanf("%d", &T);
  19. while(T--) {
  20. ans = 1e10;
  21. scanf("%lf%lf%lf%lf", &a, &b, &xx, &yy);
  22. xx = fabs(xx);
  23. yy = fabs(yy);
  24. for(double theta = 0; theta <= PI / 2; theta += 0.0002) {
  25. ans = min(ans, get_dis(theta));
  26. }
  27. printf("%.2lf\n", sqrt(ans));
  28. }
  29. return 0;
  30. }

std3.c

  1. #include <stdio.h>
  2. #include <math.h>
  3. const double eps = 1e-4;
  4. int T;
  5. double high, low, mid1, mid2;
  6. double a, b, xx,yy;
  7. double min(double a, double b) {
  8. if(a < b) {
  9. return a;
  10. }
  11. return b;
  12. }
  13. double get_dis(double theta) {
  14. double x = a * cos(theta);
  15. double y = b * sin(theta);
  16. return (x - xx) * (x - xx) + (y - yy) * (y - yy);
  17. }
  18. int main() {
  19. double PI = acos(-1.0);
  20. scanf("%d", &T);
  21. while(T--) {
  22. scanf("%lf%lf%lf%lf", &a, &b, &xx, &yy);
  23. xx = fabs(xx);
  24. yy = fabs(yy);
  25. high = PI / 2;
  26. low = 0;
  27. while(high - low > eps) {
  28. mid1 = (high - low) / 3 + low;
  29. mid2 = high - (high - low) / 3;
  30. if(get_dis(mid1) < get_dis(mid2)) {
  31. high = mid2;
  32. } else {
  33. low = mid1;
  34. }
  35. }
  36. printf("%.2lf\n", sqrt(get_dis(high)));
  37. }
  38. return 0;
  39. }

G. 送分题 贴代码

题意

用归并排序将一个完全倒序的数组从小到大排好序,输出在排序过程中总共需要进行多少次数字的大小比较操作。

数据范围

对于 的数据,
对于 的数据,
对于 的数据,

题解

延续第一届 算法竞赛的优良传统,送分题一定不是送分题,如果不看数据范围莽一发的话,送分题会一分都不给。
本题利用预处理先算出所有 的值存入数组,然后对于每一个输入,直接输出数组中的值,由于预处理的复杂度为 ,因此最多可以预处理到 ,得到 的分数,后面的直接 掉好了,如果想每一组数据都打到 的话,反而一分都拿不到。
验题人给出了一个小小的优化:

  1. int Merge(int *num, int l, int m, int r) {
  2. return r - m;
  3. }

因为本题的目的不是排序,而是计数,因此只要计算出交换次数即可,而不必将数组排序,由数组完全倒序可以知道,归并排序的每次归并,比较次数都等于 ,因此可以减少一趟排序的合并操作,将排序的复杂度从 降为 ,但是仍然只能得到 的分数。
由于这题 的范围极大,因此只有唯一解:找规律 打表预处理,存到数组里,对 组输入输出对应的答案。用给出的代码先输出 范围的答案,然后找 与答案之间的关系,这组数据有两种规律。
出题人发现的规律如下(其中 指的是 的二进制表示中数字 出现的次数):

验题人发现的规律是:

标程

std1.c

  1. #include <stdio.h>
  2. const int maxn = 10000000 + 100;
  3. int T, n;
  4. int sum[10000000 + 100];
  5. int bit(int x) {
  6. int ret = 0;
  7. for(int i = 0; i < 31; ++i) {
  8. ret += ((x >> i) & 1);
  9. }
  10. return ret;
  11. }
  12. int main() {
  13. int i;
  14. sum[1] = 0;
  15. for(i = 2; i < maxn; ++i) {
  16. sum[i] = sum[i - 1] + bit(i - 1);
  17. }
  18. scanf("%d", &T);
  19. while(T--) {
  20. scanf("%d", &n);
  21. printf("%d\n", sum[n]);
  22. }
  23. return 0;
  24. }

std2.c

  1. #include <stdio.h>
  2. const int maxn = 10000000 + 100;
  3. int T, n;
  4. int sum[10000000 + 100];
  5. int main() {
  6. int i;
  7. sum[1] = 0;
  8. for(i = 2; i < maxn; ++i) {
  9. sum[i] = sum[i / 2] + sum[(i + 1) / 2] + i / 2;
  10. }
  11. scanf("%d", &T);
  12. while(T--) {
  13. scanf("%d", &n);
  14. printf("%d\n", sum[n]);
  15. }
  16. return 0;
  17. }

H. Hack 双哈希

题意

给出了字符串 的代码以及一个字符串,要求找到另一个字符串,该字符串的哈希值与给定字符串相等,要求找到的字符串长度不超过 ,且都为小写字母。

题解

输出原字符串的话直接给 ,如果直接输出原串可以得分的话,这题就是真正的送分题了。
如果按长度和字典序暴力枚举字符串的话,按计算机 秒做 次计算的速度估算,大概要跑 年才能得到长度最短,字典序最小的字符串。
观察代码可以发现,Hash 函数调用了 Hash1Hash2 函数,由于 Hash1 得到的数字最长只有 位,Hash2 得到的数字最长大概率只有 位(有 的概率可能为 位),因此将 Hash1 的返回值乘上 再加 Hash2 的返回值,可以认为这两个哈希值是独立的。
题目给出了提示,Hash2 函数是可以进行逆运算的,因此可以通过哈希值 计算得到一个字符串,使得该字符串的哈希值恰好等于 。通过观察可以发现,Hash2 实际上是将字符串看作 进制的数字,转化为 进制的数字后对 取余,其中 a~z 分别对应 进制数的 ,但是没有字母对应的是 ,如 a 对应 z 对应 ,而下一个 进制数 aa 的哈希值为 ,正好等于 z
反推可以得到,从哈希值逆运算得到字符串的算法为:先对 取余,若余数等于 ,则当前位等于 z,并将数字减去 ,否则当前位的字母等于余数对应的字母(如 对应 a 对应 b,以此类推),不断对 取余再除以 下取整,得到一个字符串,方法类似于将 进制数转化为 进制数。
将题目给定的字符串作为 Hash2 的参数得到哈希值 ,因此对 取余之前的哈希值可能为 ,其中 为自然数。题给字符串作为 Hash1 的参数得到哈希值 ,因此可以通过枚举 所有可能的值,逆运算得到一个字符串,将字符串代入到 Hash1 中,只要返回值等于 ,该字符串就是答案。
在不爆 long long 的情况下, 可能的取值范围大约为 个字符串中存在一个用 Hash1 得到的哈希值与 相等的概率还是很大的(事实证明必然存在),此时字符串长度必然不超过 。用以下代码大概跑 秒可以跑出结果:dwdeoqlmczimq,该字符串对应的 Hash2 取余前的值为 ,题目时限为 秒,所以本地跑出这个字符串,直接 printf 得分。
以上方法仅供参考,如果有大神可以给出 的算法,请给出并收下我的膝盖。

本地暴力跑代码

  1. #include <stdio.h>
  2. int Hash1(char *str) {
  3. int ret = 0;
  4. for(int i = 0; str[i] != '\0'; ++i) {
  5. if((i & 1) == 0) {
  6. ret ^= ((ret << 7) ^ str[i] ^ (ret >> 3));
  7. } else {
  8. ret ^= (~((ret << 11) ^ str[i] ^ (ret >> 5)));
  9. }
  10. }
  11. return ret & 0x2FFFFFFF;
  12. }
  13. int Hash2(char *str) {
  14. int ret = 0;
  15. int MOD = 1000000007;
  16. for(int i = 0; str[i] != '\0'; ++i) {
  17. ret = ((long long)ret * 26 + (str[i] - 'a' + 1)) % MOD;
  18. }
  19. return ret;
  20. }
  21. void Reverse(char *str, int len) {
  22. char ch;
  23. for(int i = 0; i < len / 2; ++i) {
  24. ch = str[i];
  25. str[i] = str[len - i - 1];
  26. str[len - i - 1] = ch;
  27. }
  28. }
  29. void num2str(long long num, char *str) {
  30. int len = 0;
  31. while(num != 0) {
  32. if(num % 26 == 0) {
  33. str[len++] = 'z';
  34. num -= 26;
  35. } else {
  36. str[len++] = 'a' + num % 26 - 1;
  37. }
  38. num /= 26;
  39. }
  40. str[len] = '\0';
  41. Reverse(str, len);
  42. }
  43. int main() {
  44. char str[20];
  45. for(long long i = 0; i <= 9000000000LL; ++i) {
  46. long long x = 291377266 + 1000000007 * i;
  47. num2str(x, str);
  48. if(Hash1(str) == 752542039) {
  49. printf("%s\n", str);
  50. break;
  51. }
  52. }
  53. return 0;
  54. }

标程

  1. #include <stdio.h>
  2. int main() {
  3. printf("dwdeoqlmczimq\n");
  4. return 0;
  5. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注