SCNU ACM 2016新生赛初赛 解题报告

新生初赛题目、解题思路、参考代码一览

1001. 无聊的日常

Problem Description

两位小朋友小A和小B无聊时玩了个游戏,在限定时间内说出一排数字,那边说出的数大就赢,你的工作是帮他们统计他们获胜的次数。

Input

第一行是一个T,代表游戏的次数T(T≤1000)。每组两个整数p,y(1≤p≤\(10^{100}\),1≤y≤\(10^{100}\)),分别表示两位小朋友说出的数字。

Output

输出两个数,A和B获胜的次数。后面没有换行,仅此一题

Sample Input

2
11 22
22 11

Sample Output

1 1

解题思路

  • 签到题,但是要上天的出题人给的数据不对,造成了大量的WA。后来我点进去一看数据长度不对,放到python里len一下,数据竟然有70多位,才让大佬改成了\(10^{100}\)
  • 很简单,比较两个数字的大小并统计次数就好了。但是数字非常大,你需要用一个字符串存储。
  • 字符串大数比较,先比较长度,长度相同再从最高位向低位比较每一位的大小(直至不等)。
  • 应当注意,既然没说相等的情况,当然不应该把相等的情况也算进去。
  • 时间复杂度:O(N),常数为3以上。
  • 空间复杂度:O(N)

参考代码

  • ?:的语法是gnu c/c++独有,a ?: c等价于a ? (a) : c
#include <stdio.h>
#include <string.h>

int main() {
    int T, resP = 0, resY = 0, temp;
    char p[99]={0}, y[99]={0};
    scanf("%d", &T);
    while (T--) {
        scanf("%s%s", p, y);
        temp = (int) strlen(p) - (int) strlen(y) ?: strcmp(p, y);
        if (temp > 0)       ++resP;
        else if (temp < 0)  ++resY;
    }
    printf("%d %d", resP, resY);
    return 0;
}

1002. 写个编译器

Problem Description

写一个编译器,支持下列语言之一:
C语言 https://en.wikipedia.org/wiki/C_(language)
保证只有main函数
只使用int char main printf关键字,除字符串外只有" ( ) + - * = , 数字 字母 { } ; 空格 换行 的出现,且没有undefined behavior
保证没有判断、循环、递归

int main() {
char t;
t = 70;
printf ("%cello", t+=2);
}
Python语言 https://en.wikipedia.org/wiki/Python_(programming_language)
只使用print end关键字,除字符串外只有" ( ) + - * = , 数字 字母 空格 换行 的出现
保证没有判断、循环、递归

print (1, 4, end="+11=25")
"输出14+11=25,后面没有换行"
print (1, 4, "+11=25")
"输出14+11=25,后面有换行"
"没有print语句的表达式不会被输出"
JScript https://en.wikipedia.org/wiki/JScript
只使用WScript.Echo WScript.StdOut.Write关键字,除字符串外只有" ( ) + - * = , 数字 字母 ; 空格 . 的出现(JScript的换行比较麻烦,暂且避开)
保证没有判断、循环、递归,所有变量为数字或字符串

x=3;WScript.Echo(x+6);WScript.Stdout.Write("1\n2");
Text语言 http://esolangs.org/wiki/Text
除字符串外只有" ( ) + - * 数字 字母 空格 换行 , { }
保证没有循环、递归

Output Character H
Let t Be 3
Output String ell
If t-2 Positive {
Output Character With Ansii 111
}
If t-4 Positive {
Output Character With Ansii 10
}
BrainFuck http://esolangs.org/wiki/Brainfuck
保证只有> < + - . [ ]
每个位置可以储存0-255的数字,超过将会绕回
保证执行次数小于100 000

Input

依次输入几种语言的代码,两种语言之间以==========(10个等号)分割。保证所有代码可以正常编译执行

Output

选择一种进行编译并输出运行结果。

Sample Input

int main() {
  char t;
  t = 70;
  printf ("%cello", t+=2);
}
==========
print (1, 4, end="+11=25")
"输出14+11=25,后面没有换行"
print (1, 4, "+11=25")
"输出14+11=25,后面有换行"
"没有print语句的表达式不会被输出"
==========
x=3;WScript.Echo(x+6);WScript.Stdout.Write("1\n2");
==========
Output Character H
Let t Be 3
Output String ell
If t-2 Positive {
  Output Character With Ansii 111
}
If t-4 Positive {
  Output Character With Ansii 10
}
==========
+++++[>++++++++<-]>.

Sample Output

Hello

解题思路

  • 又是大佬的神作,看看就好。
  • 向真的写了BrainFuck的巨巨们Orz一个。
  • 注意倒数第二种语言Text,输出就是源代码本身。
  • 时间复杂度:O(N),常数大概在1~3
  • 空间复杂度:O(N)

参考代码

#include <stdio.h>
#include <string.h>

char code[(int) (3e7)], *result = code;

int main() {
    fread(code, 1, (int) (3e7), stdin);
    for (int i = 0; i < 3; ++i)
        result = strstr(result, "\n==========\n") + 12;
    *strstr(result, "\n==========\n") = 0;
    printf("%s", result);
    return 0;
}

1003. 手机密码

Problem Description

某天CZJ在玩辣鸡游戏,玩着玩着发现手机被hack了,重启之后发现界面上出现了一个数字和一个密码框。
CZJ试了半天发现,当输入的正整数满足以下条件时,界面上不会弹出可恶的错误:

  1. 它的二进制形式中1的数量和显示数字的(二进制中1数量)相同;
  2. 十进制大于显示的数字;
  3. 在所有可行答案中最小。

可悲的是又跳出一个数字。CZJ算了几个之后觉得好麻烦,你可以帮他算一下吗?

Input

第一行是一个T,代表数据的组数T(T≤100001)。接下来是T行,每行有一个数字N,表示显示的数字(1≤n≤1000000000)。

Output

针对每一个数字输出对应密码。

Sample Input

2
1
5

Sample Output

2
6

Hint

1的二进制表达为1,2的二进制表达为10,5的是101,6的是110

解题思路

  • 首先只考虑第一个条件。统计数字N的二进制表示中,“1”的数量C。然后输出一个数X,使得X的二进制表示是由C个连续的“1”构成。
  • 再考虑第二个条件,要求输出的X要比N大。那就从右边找极值,从右到左扫描N的二进制,先扫过一段连续的“0”,再扫过一段连续的“1”,之后再第一次扫描到“0”时停止。如N=92(0101 1100)。把这一位0置为1,下一位1置为0(为了保证“1”的个数不变),像这样:X=108(0110 1100)。这样输出X的确要比原数N大。
  • 最后考虑第三个条件,在所有可行方案中最小。我们再次考察X=108(0110 1100),由于X=108具有N=88所没有的更高位的“1”,所以X一定比N大,而不论后面低位的数字如何变化。于是重排X=99(0110 0011),也就是将比变换位低位的1全部移到右边去。
  • 时间复杂度:O(d)=O(logN)
  • 空间复杂度:O(1)

参考代码1

  • __builtin_ctz函数是gnu c/c++独有,用于计算从右起第一个“1”之后的“0”的个数,参数为0时是ub;
  • 也可以用__builtin_ffs函数找到从右起第一个“1”的位置;
  • 然而这些函数看看就好,记得可以用,还是推荐自己算。
#include <stdio.h>

int main() {
    int T, N, a, b, t;
    scanf("%d", &T);
    while (T--) {
        scanf("%d", &N);
        t = a = N & -N;
        do a <<= 1; while (a & N);
        b = (N & (~(-a))) >> (__builtin_ctz(t) + 1);
        N = N & -a ^ a ^ b;
        printf("%d\n", N);
    }
    return 0;
}

参考代码2

#include <stdio.h>

int main() {
    int T, N, a, b;
    scanf("%d", &T);
    while (T--) {
        scanf("%d", &N);
        a = 0, b = 0;
        while (!(N & 1)) N >>= 1, ++a;
        while (N & 1) N >>= 1, ++b;
        printf("%d\n", ((N | 1) << (a + b)) & (~((1 << (a + b)) - 1)) | ((1 << (b - 1)) - 1));
    }
    return 0;
}

1004. Zyj大逃亡

Problem Description

Zyj打比赛时又没做出题来,还写崩了一道题,他的队友们和SCNU的大佬们都跑来追杀他啦!Zyj在逃亡过程中和大佬们一起穿越到了LOL世界中。大佬们启动了“疾走”技能,很快就把Zyj围堵到了一个包围圈中。
Zyj拥有一个被动技能,当不向敌人走动时能获得飞一般的速度,从而逃出包围圈。已知包围圈的半径为\(R\),可以设Zyj所在位置为原点O,大佬们都恰好站在圆上一点,并知道每位大佬的坐标位置为\(P_i(x_i, y_i)\)。显然每位大佬之间都隔着一段圆弧,Zyj可以且仅可以从长度\(L \geqslant \frac12 R\)的圆弧的中点逃出以保证他的被动技能生效。若不存在这样的圆弧(即长度都太短)则Zyj无路可逃。
注意,如果Zyj的可选最长圆弧不唯一,即使长度足够,也认为Zyj无论走哪个方向都不背向敌人,Zyj还是无路可逃。

Input

第一行只有一个正整数\(T\)\(T \leqslant 1000\)),代表了\(T\)个情形。
接下来有\(T\)组输入,每组输入中包含两个正整数\(N\)\(N>3\))和\(R\)\(0<R<1000\)),分别代表包围圈的半径和追杀Zyj的大佬数量。接下来\(N\)行每行有\(2\)个数字,分别代表\(N\)个大佬的坐标\((x_i, y_i)\),保证所有的\(x_i^2+y_i^2=R^2\)成立,提供数据的精度保证7位有效小数(舍入后)。保证数据读得完。

Output

对于每个Zyj逃亡的情形,如果Zyj能逃出来,则输出Zyj可逃离的最长圆弧的中点的坐标(保证答案唯一,且保留4位小数);如果Zyj无路可逃,请输出"Zyj has been slain"(不包括双引号)。

Sample Input

2
4 25
0 25
-25 0
-0 -25
25 -0
4 25
0 25
0 -25
20 15
20 -15

Sample Output

Zyj has been slain
-25.0000 0.0000

解题思路

  • 我出的题。题目有修改,“如果Zyj的可选最长圆弧不唯一”原本的描述是“如果Zyj能选择的所有圆弧都等长”,但是多事的oyk说看不懂,结果改了描述后好像变难了。我的代码仍然是“所有等长”的版本,如果要判“最长不唯一”,要另开数组标记一下浮点数。
  • 如果用笛卡尔坐标系,不是不能做,挺难的。考虑极坐标系,由于坐标都在圆上,长度均等于R。每个大佬的直角坐标对应极坐标角度angle,对angle进行排序,两两相减得到每段圆弧所对圆心角theta,遍历一遍这个theta,最后有答案就除以2再转直角坐标。
  • 注意由于是个,还要算上第一个和最后一个坐标之间的圆弧。
  • 正确姿势做法:极角排序
  • 奇怪的错误点:
    1. 完全不会测试浮点数,判断相等时有精度问题;
    2. 完全不会比较浮点数,判断大小时比较错误。
  • 时间复杂度:O(NlogN)

参考代码

  • C语言在stdlib.h里有个qsort排序函数
  • C++在algorithm中(STL)有个std::sort排序函数
  • 不要干手写冒泡排序这种奇怪的事情,手写快排也不要
  • 浮点数由于精度误差,不能直接比较相等,要设置一个较小的允许误差,小于这个误差可认为等于0
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <stdlib.h>

//做自己出的题WA了是一种怎样的体验?Zyj的回答,获得0个赞同。
const double PI = acos(-1.);

int angleCount;
double angleArray[10000];

void init() {
    angleCount = 0;
    memset(angleArray, 0x0, sizeof(angleArray));
}

int N, R;

void read() {
    double x, y;
    scanf("%d%d", &N, &R);
    for (int i = 0; i < N; ++i) {
        scanf("%lf%lf", &x, &y);
        angleArray[angleCount++] = atan2(y, x);
    }
}

int fcmp(double a, double b) {
    /*if (fabs(a - b) <= 1e-7)*/
    if (fabs(a - b) / fabs(a) <= 1e-7 ||
        fabs(a - b) / fabs(b) <= 1e-7)
        return 0;
    if (a < b) return -1;
    /*if (a > b)*/ return 1;
}

int compareDouble(const void *a, const void *b) {
    return fcmp(*(double *) a, *(double *) b);
}

void work() {
    qsort(angleArray, (size_t) angleCount, sizeof(double), compareDouble);
    angleArray[angleCount++] = angleArray[0] + 2. * PI;

    int isNotSame = 0;
    int rangeUpperIdx = 1;
    double maxRange = angleArray[rangeUpperIdx] - angleArray[rangeUpperIdx - 1];
    for (int i = 2; i < angleCount; ++i) {
        double temp = angleArray[i] - angleArray[i - 1];
        int cmpJudge = fcmp(temp, maxRange);
        if (cmpJudge) ++isNotSame;
        if (cmpJudge > 0) {
            maxRange = temp;
            rangeUpperIdx = i;
        }
    }
    if (isNotSame && fcmp(maxRange, 0.5) > 0) {
        double theta = (angleArray[rangeUpperIdx] + angleArray[rangeUpperIdx - 1]) / 2.;
        printf("%.4f %.4f\n", R * cos(theta), R * sin(theta));
    } else {
        printf("Zyj has been slain\n");
    }
}

int main() {
    int T;
    scanf("%d", &T);
    while (T--) {
        init();
        read();
        work();
    }
    return 0;
}

1005. Czj数数

Problem Description

自从Czj上了初中,已经不再满足于从1数到100了,他打算来个高难度的挑战。
设a1=1,a2=11,有数列ai,任意1<i≤n,元素ai−1与其自身的最后一位数字组成了新的数字X(如233将会变成2333),而ai是X的最大质因子。由于Czj很懒,数到2333就觉得无聊而跑去写gc了。现在请你帮他完成这未竟的事业。

Input

第一行是一个正整数T(T<100),代表接下来有T次询问。
接下来的T行,每一行输入一个正整数i(1≤i≤104)。

Output

对于每次询问,你应该求出数组中的第i个元素ai,并以"Case #t: ai"的格式单独输出一行,不包括双引号。其中#t代表询问的编号。

Sample Input

3
5
6
7

Sample Output

Case #1: 23
Case #2: 233
Case #3: 2333

解题思路

  • 还是本菜鸡出的题。
  • 遇事不决打个表。如果你打了表,你就会发现生活的美好——这里面原来是有循环节的。从第5个数开始,到第30个数,是一个循环节。
  • 一般这种一眼看过去是递推,又没找到公式,干脆想都不要想直接打表出来,找有没有循环节,没有循环节再找规律。
  • 怎么找一个数的最大质因子呢?不需要素数筛,试除法就行。当然你开心的话,可以上Eratothenes筛或欧拉筛打素数表。试除法打素数表是会超时的。当然这里用到的素数还不大于100。
  • 时间复杂度:O(1)或O(\(M\sqrt{N}\))或O(\(M\log \log N\)),M=??

求最大质因子

/**
 * find out the largest prime factor of given num.
 */
int largestFactorOf(int num) {
    int iterateNumber = num;
    int q = (int) sqrt(num) + 1;
    int iterateFactor = 2;
    while (iterateFactor <= q) {
        while (iterateNumber % iterateFactor == 0)
            iterateNumber /= iterateFactor;
        if (iterateNumber == 1)
            break;
        ++iterateFactor;
    }
    return iterateNumber == 1 ? iterateFactor : iterateNumber;
}

参考代码

#include <stdio.h>

/**
 * http://oeis.org/A195201
 * Description: for a(1) = 1, a(n) is the largest prime factor of
 * the number which is made up from a(n-1) and its last digit.
 * Input: T, and T lines of any n.
 * Output: a(n) for every n.
 * */

const int fixedSection[4] = {
        1, 11, 37, 29
};
const int cycleSection[26] = {
        23, 233, 2333, 23333, 661, 601, 6011, 6679,
        997, 907, 313, 241, 2411, 47, 53, 41, 137,
        17, 59, 599, 857, 953, 9533, 13619, 19457, 821
};
int T, n;

void read() {
    scanf("%d", &n);
}

void work(int caseCount) {
    printf("Case #%d: %d\n", caseCount, n <= 4 ? fixedSection[n - 1] : cycleSection[(n - 5) % 26]);
}

int main() {
    scanf("%d", &T);
    for (int i = 1; i <= T; ++i) {
        read();
        work(i);
    }
    return 0;
}

1006. 飞来飞去的Zyj

Problem Description

Zyj在接电路板。他给出了要连接的线的列表,并按顺序进行连接。但是他不绕线,如果两个点连成的线段不与任何画线相交,就在上面把线画出来,否则就直接上飞线(即这根线不画在电路板上,而是在空中连接)。问,Zyj一共飞了几条线?
注意,如果其中一条线经过另一条线的端点,视为相交;但如果两条线在端点上相接,视为不相交。部分重叠视为相交,除非在端点上相接

Input

第一行T为数据组数(小于100)
每组数据的第一行n为连线数量(小于100),后面紧跟着n行(x1,y1)-(x2,y2)表示端点坐标。坐标值的绝对值小于100.

Output

对每组数据,输出Case #d: q,其中d为数据编号,q为飞线数量。

Sample Input

2
7
(1,2)-(4,2)
(4,2)-(4,1)
(4,1)-(2,1)
(2,1)-(2,4)
(2,4)-(3,4)
(3,4)-(3,3)
(1,3)-(3,3)
4
(1,1)-(7,1)
(2,1)-(2,2)
(3,1)-(4,1)
(6,1)-(7,1)

Sample Output

Case #1: 1
Case #2: 2

样例解释:

4  ┎┐   
3 ─╂┘
2 ─╂─┐
1 ┗─┘
1234

2 ┃
1 ─┸────────
  ━━  ──(这行和上一行是叠在一起的)
1234567
其中粗线表示飞线

解题思路

  • 讲真,这题我也不会做。反正又是大佬的神作。
  • 后来搜了下,线段判交是有技巧的,常用的是向量积;也可以直接解析几何解方程。这里用了随便一搜能搜出来的快速排斥+跨立试验。
  • 需要注意的是1.按顺序连接;2.重叠相交;3.端点连接强制不相交;4.应当标记飞过的线。
  • 时间复杂度:O(\(N^2\))

参考代码(因为我不会做,直接用C++了)

#include <stdio.h>
#include <string.h>
#include <algorithm>

using std::max;
using std::min;

inline int feq(const double &a, const double &b) {
    return fabs(a - b) < 1e-7;
}

inline int fneq(const double &a, const double &b) {
    return !feq(a, b);
}

inline int fgeq(const double &a, const double &b) {
    return feq(a, b) || a > b;
}

struct Point {
    double x, y;

    Point() {}

    Point(double x, double y) : x(x), y(y) {}

    int operator!=(const Point &p) const {
        return fneq(x, p.x) || fneq(y, p.y);
    }
};

struct Line {
    double x, y;

    Line() {}

    Line(const Point &P0, const Point &P1) : x(P1.x - P0.x), y(P1.y - P0.y) {}

    double operator^(const Line &l) const {
        return x * l.y - y * l.x;
    }
};

typedef Point Segment[2];

inline int quickReject(Segment &A, Segment &B) {
    return max(A[0].x, A[1].x) >= min(B[0].x, B[1].x) &&
           max(A[0].y, A[1].y) >= min(B[0].y, B[1].y) &&
           max(B[0].x, B[1].x) >= min(A[0].x, A[1].x) &&
           max(B[0].y, B[1].y) >= min(A[0].y, A[1].y);
}

inline int quickCross(Segment &A, Segment &B) {
    return (Line(B[0], A[0]) ^ Line(B[0], B[1])) * (Line(B[0], B[1]) ^ Line(B[0], A[1])) >= 0 &&
           (Line(A[0], B[0]) ^ Line(A[0], A[1])) * (Line(A[0], A[1]) ^ Line(A[0], B[1])) >= 0;
}

int flown[101];
Segment seg[101];

int main() {
    int T, N;
    scanf("%d", &T);
    for (int cse = 1; cse <= T; ++cse) {
        int res = 0;
        memset(flown, 0x0, sizeof(flown));
        scanf("%d%*c", &N);
        for (int i = 0; i < N; ++i) {
            scanf("%*c%lf%*c%lf%*c%*c%*c%lf%*c%lf%*c%*c", &seg[i][0].x, &seg[i][0].y, &seg[i][1].x, &seg[i][1].y);
            for (int j = 0; j < i; ++j)
                if (quickReject(seg[j], seg[i]) && quickCross(seg[j], seg[i]) &&
                    seg[j][0] != seg[i][0] && seg[j][0] != seg[i][1] &&
                    seg[j][1] != seg[i][0] && seg[j][1] != seg[i][1] && !flown[j]) {
                    flown[i] = 1;
                    ++res;
                    break;
                }
        }
        printf("Case #%d: %d\n", cse, res);
    }
    return 0;
}

1007. 灌水

Problem Description

CZJ和L2M在车上闲着没事拿了个水杯灌水,规定杯子的体积P和每次可以倒进杯子的水的最大体积Y,两个人轮流向杯子灌整数体积的水,最后灌满杯子的人获胜。
CZJ看起来很想赢的样子,L2M宽宏大量的给了先手。但是CZJ这么菜,你能教下CZJ起手要灌多少才能必胜吗?

Input

第一行是一个T,代表数据的组数T(T≤20000)。每组两个整数p,y(1≤p≤10000000,1≤y≤10000000)。

Output

对于每组数据输出CZJ起手灌水的数量,如果CZJ赢不了,打出GG。

Sample Input

2
3 1
2 1

Sample Output

1
GG

解题思路

  • 首先这是一个入门级博弈问题。不知道博弈的也没关系,如果你有耐心,在纸上演算一下6以内的情况,就会发现规律。
  • 这个问题是巴什博奕先手赢。我们总考虑P>V,否则先手必胜的。
  • 我们先找一个先手必败态:P=Y+1。无论先手怎么倒水,总不能倒满,而后手总能把杯子倒满,所以这种情况先手必败。
  • 在其他情况下,能不能想办法把这个必败态转移给对手呢?考虑只有两局,第一局先手先倒M的水,后手和第二局先手总共倒N=P-M的水,并且要保证第二局先手胜。
  • 我们发现这相当于只有一局——不管先手倒多少体积为M的水,就当水杯体积是P-M好了,这样原本的后手等价成为了新的先手。如果剩下的体积刚好N=Y+1,那么原本的后手就陷入了先手必败困局
  • 如果除去一开始倒的M水,每一局都总共倒Y+1的水呢?因为Y<Y+1<2Y,新的先手永远最多只能倒1<=Z<=Y的水,而新的后手总能控制局面的变化,即他能倒Y+1-Z<=Y的水。控制每次两人总共倒Y+1的水,就赢了。
  • 为什么一定是Y+1,不能是+2、+3吗?因为再多的话,先手就不是必败了,先手总能取一个较小值使得后面剩下Y+1。
  • 所以一开始要抢先倒P%(Y+1)的水,后面总共倒的水体积是(Y+1)的倍数。

参考代码

#include <stdio.h>

int main() {
    int p, y;
    scanf("%d", &p);
    while (~scanf("%d%d", &p, &y))
        (p %= (y + 1)) ? printf("%d\n",p) : puts("GG");
    return 0;
}

### 1008. 和

Problem Description

输入几个数字,求和。

Input

每行一个样例,由正整数组成,空格分开

Output

对每个样例,输出和。保证结果小于\(10^9\)

Sample Input

1 1
1 2

Sample Output

2
3

解题思路

  • 签到题,注意题面的“几个数字”
  • 用EOF判输入结束。可以用一个小char判断换行。

参考代码

#include <stdio.h>

int main() {
    int res, a, c;
    while (~scanf("%d", &res)) {
        while (scanf("%c", &c), c != '\n') {
            scanf("%d", &a);
            res += a;
        }
        printf("%d\n", res);
    }
    return 0;
}

1009. 开饭

Problem Description

CZJ的舍友请CZJ吃饭,作为舍友CZJ是不会放过吃穷舍友的机会,但自己身体又不好,不能吃太多也不能吃太少。假设每样食物都有一个饱腹度和价值,请你帮他算下刚好吃饱的情况下最多可以吃他舍友多少钱。

Input

第一行是一个T,代表数据的组数T(T≤100001)。每组两个整数p,y(1≤p≤1000,1≤y≤100),分别表示食物的数量和CZJ的饱腹度。
然后p个数\(Q_{p_i}\)表示第\(p_i\)个食物的价值,之后p个数\(M_{p_i}\)表示吃第\(p_i\)个食物得到的饱腹度。(1≤\(Q_{p_i}\)≤10000,1≤\(M_{p_i}\)≤1000)。

Output

对于每一个样例输出CZJ能吃下的最大价值。

Sample Input

1
5 100
11 6 7 5 6
40 50 60 20 30

Sample Output

18

解题思路

  • 0-1背包问题求最大值。入门级dp。
  • 对于这题有很多人过没有意外。只要你搜一下什么叫“动态规划”或者“dp”,把0-1背包的递推往这一套,就A了。
  • 推荐学习资料:“背包九讲”、“动态规划32讲”。

参考代码

#include <stdio.h>
#include <string.h>

#define MAXN 1010
int C[MAXN], W[MAXN], dp[MAXN];

void init() {
    memset(C, 0x0, sizeof(C));
    memset(W, 0x0, sizeof(W));
    // dp数组初始值要足够小。这里一个int是0x80808080
    memset(dp, 0x80, sizeof(dp));
    dp[0] = 0;
}

void getMax(int &a, int b) {
    if (a < b) a = b;
}

int N, V;

void read() {
    scanf("%d%d", &N, &V);
    for (int i = 0; i < N; ++i) scanf("%d", W + i);
    for (int i = 0; i < N; ++i) scanf("%d", C + i);
}

void work() {
    for (int i = 0; i < N; ++i)
        for (int v = V; v >= C[i]; --v)
            getMax(dp[v], dp[v - C[i]] + W[i]);
    printf("%d\n", dp[V]);
}

int main() {
    int T;
    scanf("%d", &T);
    while (T--) {
        init();
        read();
        work();
    }
    return 0;
}

1010. Zyj消极比赛

Problem Description

ACM比赛剩下最后10分钟,其他人都已经收拾好东西准备走人,Zyj才从睡梦中醒来。Zyj可以看到所有其他人的过题情况,他希望得到的名次在a到b之间,问有几种可选择的方案?(假设其他人的提交时间即使加上罚时也早于Zyj的提交,毕竟剩下10分钟了都)

Input

第一行为T(0<T<100),代表有T组数据。
每组数据中第一行为两个数字m n a b,由空格隔开,代表这次比赛有m道题(由于字母数量的限制,0<m<27),n个其他人,Zyj希望得到的名次x满足a<x<b (-1<n,a,b<1000)
下面n行为每个人每道题的通过情况,1为已通过,0为未通过

Output

对每个样例,输出Case #k: ans,其中k为样例编号,ans为方案数量。

Sample Input

2
26 1 0 2
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0
26 1 0 2
0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0

(Case #1是25个1 1个0,#2是1个0 24个1 1个0)

Sample Output

Case #1: 1
Case #2: 27

Hint

样例解释:
Case #1中Zyj必须AK(全部题都做出来)才可以得到大于0小于2的名次,即第1名。不要怕他做不完,那可是Zyj
Case #2中Zyj可以AK,也可以任意选一道不做

ACM排名规则见http://scnuacm2015.sinaapp.com/php/presentation.php

解题思路

  • 对于这种题我只能无语。
  • 这里有个隐含条件,因为Zyj是最后做题,罚时爆炸,所以无论他做多少题,都是同题数的最后一名
  • 然后累加出每个人过题的数量,进而统计过0≤x≤m题的人数,从最高名次到最低名根据Zyj想要的名次去求组合数就好。
  • 一些错误点:
    1. 没有处理好边界,对于不可能存在/达到的排名,计算结果不为0;
    2. 同题数计算排名时没有考虑zyj是最后做题;
    3. 没有加上当有人过0题时,zyj拿最后一名也可以过0题的1种情况;
    4. 并列排名不顺延,比如1题的最后一名可能是x,那么0题的并列排名是x+1而不是n;
    5. 有没看懂题的,把过题总数直接作为排名(也是没考虑过题时间会影响排名);
    6. 还有一些奇怪的错误..不会算组合数or阶乘..有的没考虑除0..有的没考虑计算过程不整除..有的..我也不知道。
  • 时间复杂度:\(O(m*m)\)\(O(m+m*m)\)(公式法预处理)或\(O(m+m)\)(预处理阶乘及其逆元)

参考代码1

#include <stdio.h>
#include <string.h>

/*
 * count[k] 记录了过了k题的人数
 */
int m, n, a, b;
int count[27];

void init() {
    memset(count, 0x0, sizeof(count));
}

void read() {
    scanf("%d%d%d%d", &m, &n, &a, &b);
    for (int i = 0; i < n; ++i) {
        int acc = 0, temp;
        for (int j = 0; j < m; ++j) {
            scanf("%d", &temp);
            acc += temp;
        }
        // 实际上count[0]没有利用价值
        ++count[acc];
    }
}

// 求组合数 - 魔幻除法(可以想想为什么能整除)
int C(int n, int x) {
    int res = 1;
    if (n - x > x) x = n - x;
    n -= x;
    for (int i = 1; i <= n; i++)
        res = res * (x + i) / i;
    return res;
}

int work() {
    int res = 0, rank = 1;
    for (int i = m; i > 0; --i) {
        // zyj 做i题能拿的名次
        rank += count[i];
        if (a < rank && rank < b)
            res += C(m, i);
    }
    // 就算做0题,zyj也是拿同做1题一样的排名
    if (a < rank && rank < b)
        res += 1;
    return res;
}

int main() {
    int T;
    scanf("%d", &T);
    for (int cse = 1; cse <= T; ++cse) {
        init();
        read();
        printf("Case #%d: %d\n", cse, work());
    }
    return 0;
}

参考代码2

#include <stdio.h>
#include <string.h>

/*
 * count[k] 记录了过了k题的人数
 */
int m, n, a, b;
int count[27];

void init() {
    memset(count, 0x0, sizeof(count));
}

void read() {
    scanf("%d%d%d%d", &m, &n, &a, &b);
    for (int i = 0; i < n; ++i) {
        int acc = 0, temp;
        for (int j = 0; j < m; ++j) {
            scanf("%d", &temp);
            acc += temp;
        }
        // 实际上count[0]没有利用价值
        ++count[acc];
    }
}

// 求组合数 - 公式预处理法
int C[27][27];
const int MOD = 1000000007;
void pre() {
    C[0][0] = C[1][0] = C[1][1] = 1;
    // 公式 C(n,x) = C(n-1,x-1) + C(n-1,x)
    for(int n = 2; n <= 26; n++) {
        C[n][0] = C[n][n] = 1;
        for(int x = 1; x < n; x++)
            C[n][x] = (C[n-1][x-1] + C[n-1][x]) % MOD;
    }
}

int work() {
    int res = 0, rank = 1;
    for (int i = m; i > 0; --i) {
        // zyj 做i题能拿的名次
        rank += count[i];
        if (a < rank && rank < b)
            res += C[m][i];
    }
    // 就算做0题,zyj也是拿同做1题一样的排名
    if (a < rank && rank < b)
        res += 1;
    return res;
}

int main() {
    pre();
    int T;
    scanf("%d", &T);
    for (int cse = 1; cse <= T; ++cse) {
        init();
        read();
        printf("Case #%d: %d\n", cse, work());
    }
    return 0;
}

1011. Oyk剪纸

Problem Description

Oyk要把一张矩形纸剪成N张一样的矩形不留剩余,使用的方法为
1.从一个方向剪若干(可能为0)刀
2.从与其垂直的方向再剪若干刀
那么他至少要剪几刀?

Input

多(少于1000)组样例,每行一个数字N(大于0小于1亿)。

Output

对每个样例,输出一行结果。

Sample Input

4
5
60

Sample Output

2
4
14

Hint

解释:

┌┬┐┌┬┬┬┬┐  
├┼┤└┴┴┴┴┘
└┴┘

解题思路

  • 首先剪的方向只能是与纸边平行或垂直。斜着剪会产生剩余,而且也产生负收益(浪费了剪的次数)。
  • 于是问题分解成了\(N=x * \frac{N}{x}\)\(r=(x-1)+(\frac{N}{x}-1)\),要让\(r\)尽可能小。不记得什么不等式了。
  • 直接任性求导,\(r'=1-\frac{N}{x^2}=0\)\(x=\sqrt{N}\)\(r\)取得最小值。但注意x要是整数(不可能剪半刀)。
  • 时间复杂度:O(\(\sqrt{N}\)),最坏情况由N是素数产生

参考代码

#include <stdio.h>
#include <math.h>

int main() {
    int N, q;
    while (~scanf("%d", &N)) {
        q = (int) sqrt(N);
        for (; q > 1; --q)
            if (N % q == 0) break;
        printf("%d\n", q + N / q - 2);
    }
    return 0;
}



出题及解题总结

我出的两题

  • 1004、1005都是我出的题。设计是一简单一难的。
  • 1004极角排序我想对新生来说还是挺难想到并做出的,事实上我出数据都快出成傻逼了;
  • 1005循环节打表可能我太高估新生水平了(?),因为我并没有出1e9的数据去卡素数表,而且数据范围设置非常小,试除法也不至于TLE,基本上是随意过的。理想情况是再不济一堆WA或TLE吧,结果伤心的发现没人做(逃)。

整体情况总结

  1. 首先看统计数据,1001的WA接近500,这里Czj应该背锅,题面数据范围与实际不符。还有一个不输出回车的坑爹设定,强行卡题意,WA得惨不忍睹,严重打击了部分同学信心。
  2. 统计数据还有第二个明显峰度在1003的OLE上。因为我看不到提交代码,没法分析。
    • 有可能是hdoj的OLE是这么判的:输出超过标准答案即判OLE。如果是这样,很多OLE应当划到WA里面。
    • 但是也有反映将C++的输出换成C语言的输出即可通过。这应当是hdoj的锅了。
  3. 还有两个WA小高峰位于1003、1008。
    • 1003的确是很多同学根本没接触到位运算的知识。
    • 而1008也是强行卡题意,给的样例不规范,还不如只给一个。另外也有很多同学并不知道EOF判输入结束。
  4. 从所有题目通过率来看,
    • 1001、1003、1005、1007、1008、1011是正常水平在七天时间内能做出的。
    • 1009尽管是入门级dp,但也涉及了算法,虚高是因为生源有部分接触过OI,以及的确简单,能间接搜到答案。
  5. 从难度上看,
    • 1002爆冷是正常,原因大家都懂。
    • 1004的确算是较难的了,之后改题面应该称得上全场最难。
    • 1006爆冷正常,因为我也不会做(逃),好吧是因为涉及了计算几何,不应该出现在新生赛中。
    • 1010爆冷是意料之外,也是情理之中,这种卡题意挖坑的题必须找出题人背锅。
  6. 1001、1008是两个签到题。预计决赛名额定在解出两题以上。
  7. 我校出题质量一年比一年差,迟早要完。

历届新生赛&别人的新生赛

  1. 华南师大2014新生赛:http://3.scnuacm2015.sinaapp.com/?p=38
  2. 广工2016新生决赛网络同步赛重现:http://gdutcode.sinaapp.com/contest.php?cid=1051
  3. ....

建议、意见、吐槽

欢迎在下方评论区提出问题,12月内我都会回复and更新。




本文基于知识共享许可协议知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议发布,欢迎引用、转载或演绎,但是必须保留本文的署名BlackStorm以及本文链接http://www.cnblogs.com/BlackStorm/p/SCNUCPC_2016_For_Freshman_Preliminary_Solution.html,且未经许可不能用于商业目的。如有疑问或授权协商请与我联系

posted @ 2016-12-14 19:07  BlackStorm  阅读(1995)  评论(0编辑  收藏  举报