Zuma


CST LAB1

Zuma

代码 1

  • 错误类型: Runtime Error

  • 错误原因

void play(int rank) {
    int left = rank;
    int right = rank;
    char color = a.at(rank);

    while (left >= 0 && a.at(left) == color) --left;
    left += 1;
    while (right < a.size() && a.at(right) == color) ++right;

    int size = right - left;
    if (size >= 3) {
        a.erase(left, size);
        play(left - 1);
    }
}

由于 play 函数并未显式调用 return, 在这里 left 的范围为 [0, rank].

因此, 若 left 在函数体内变为 0, 那么在尾部调用 play(left - 1) 时便会访问到越界的内存 a.at(-1), 产生 Runtime Error.

  • 测例构造思路

AAB 经第一次消除后变为 B, 因为消除发生在串的最前部, 此时 play 函数内 left = 0, 接下来调用 play(left - 1) 时便会访问到越界的内存 a.at(-1), 产生 Runtime Error.

  • 构造测例
AAB
1
1 A

代码 2

  • 错误类型: Runtime Error

  • 错误原因

void play(int rank) {
    ...
    if (size >= 3) {
        a.erase(left, size);

        int next = left;
        if (left - 1 >= 0) next = left - 1;

        play(next);
    }
}

此处 play 函数处理了 left = 0 的情况. 但如果经过消除, 在函数尾部串 a 为空, 此时 left = 0, 发生调用 play(0) 便会越界访问了空串 a 的元素 a.at(0), 产生 Runtime Error.

  • 测例构造思路

AA 经第一次消除后变为空串, 因为消除发生在串的最前部, 此时 play 函数内 next = left = 0, 接下来调用 play(next) 时便会访问到越界的内存 a.at(0), 产生 Runtime Error.

  • 构造测例
AA
1
1 A

代码 3

  • 错误类型: Time Limit Exceeded

  • 错误原因

void play(int rank) {
    ...
    if (size >= 3) {
        a.erase(left, size);

        if (left < a.size()) {
            play(left);
        }
    }
}

这份代码在逻辑上是正确的, 但是由于采用数组实现, 时间复杂度正比于输入数据的规模. 因为数组的插入与删除操作复杂度为 $O(n)$, 那么当输入规模过大时, 会导致 Time Limit Exceeded.

  • 测例构造思路

构造长度为 500000 的串 a, 并在串 a 的头部进行 500000 次插入操作, 导致超时.

代码 4

  • 错误类型: Wrong Answer

  • 错误原因

void play(int rank) {
    ...
    while (left > 0 && a.at(left) == color) --left;
    while (right < a.size() && a.at(right) == color) ++right;

    int size = right - left;
    if (size >= 3) {
        a.erase(left, size);

        if (left >= 0 && left < a.size()) {
            play(left);
        }
    }
}

play 函数的实现事实上并未能访问 a.at(0) 的具体元素, 注意到 left 可被赋值为 0, 此时在 erase 操作中会删去 a.at(0) 位置的元素, 如果在此时进行了消除则会导致 Wrong Answer.

  • 测例构造思路

BAC 经第一次插入 Aleft = 0, right = 3, 错误地在串头部发生消除, 串 a 变为 C, 产生 Wrong Answer.

  • 构造测例
BAC
1
1 A

代码 5

  • 错误类型: Wrong Answer

  • 错误原因

int main(){
    cin >> a;
    int m = 0;
    cin >> m;
    ...
}

主函数在读取字符串 a 时使用了 cin, 因为 cin 并不能读取空串, 因此这种方式不具有鲁棒性. 这种情况下, 程序会将本属于 m 的值读入 a, 进而导致错误.

  • 测例构造思路
int main() {
    ...
    for(int i = 0; i < m; ++i) {
        cin >> rank >> color;
        a.insert(a.cbegin() + rank, color);
        play(rank);
    }

    cout << a << endl;

    return 0;
}

将输入串 a 置空, 程序会将本属于 m 的值 2 读入 a, 将值 0 读入 m, 此时循环会因 m = 0 被直接跳过执行 cout << a << endl, 产生 Wrong Answer.

  • 构造测例
 
2
0 A
0 B

代码 6

  • 错误类型: Wrong Answer

  • 错误原因

void play(int rank, char ch) {
    Rank pos = find(rank);
    char *cur = &get(pos);
    int succ_len = plen[pos.first] - pos.second;
    if (succ_len > 0) {
        memmove(cur + 1, cur, succ_len);
    }
    *cur = ch;
    alen++;
    plen[pos.first]++;
    ...
}

play 函数在插入字符的时候并未考虑是否会超出数组大小, 直接执行 memmove 操作. 由于字符串采用二维数组 p[1 << 12][plen_bound] 形式存储, 占用了一块连续的内存. 若一直进行插入操作而不发生消除, 那么在单个数组到达最大容量后便会覆盖下个数组的内容, 进而导致输出串 a 时产生 Wrong Answer.

  • 测例构造思路

只需向字符串头部不停执行插入操作而不产生消除即可, 在单个数组到达最大容量后便会覆盖下个数组的内容, 进而导致输出串 a 时产生 Wrong Answer.

代码 7

  • 错误类型: Wrong Answer

  • 错误原因

void play(int rank, char ch) {
    // 计算需要消除的开区间 (l, r)
    ...
    while (1) {
        while (l.first >= 0 && get(l) == ch) {
            l.second--;
            dis++;
            if (l.second < 0 && l.first >= 0) {
                l.first--;
                if (l.first >= 0)
                    l.second += plen[l.first];
            }
        }
        ...
    }
}

play 函数中处理左右两侧 Rank 时, 会对左右侧 Rank 进行移动. 当需要消除的字符不全处于同一数组中时, 会进行判断并将 Rank 移动指向不同的数组.

但是在移动左侧 Rank l 时使用 if 进行判断, 每移动一个字符的位置, l 最多改变一个数组的指向. 因此若存在类似 CBA| |ABC 的情况 (使用 “|” 分隔不同的数组), 向第三组头部插入字符 A, 经过一个 if 循环后, l 仅能移动到第二组而非第一组.

此时 l.first = 2, l.second = -1, 由于存储字符串使用二维数组的结构, 操作 get(l) 会访问到 p[1][plen_bound - 1] = p[1][4095], 而非预期中的 p[1][plen[1] - 1] = p[1][2], 进而导致错误地认为不能消除, 产生 Wrong Answer.

  • 测例构造思路

输入 4099 位的字符串 a 使得 a 能被存储入三个数组 p[0]p[1]p[2], 然后通过一系列插入消除使得 p[1] 为空, 此时 p[0] 的末字符与 p[2] 的首字符均为 B. 最后向 p[2] 首部插入字符 B, 但是程序错误地认为不能消除, 产生 Wrong Answer.

代码 8

  • 错误类型: Wrong Answer

  • 错误原因

void play(int rank, char ch) {
    ...
    while (l.first >= 0 && get(l) == ch) {
        l.second--;
        dis++;
        while (l.second < 0 && l.first >= 0) {
            l.first--;
            if (l.first >= 0)
                l.second += plen[l.first];
        }
    }
    while (r.first < pn && get(r) == ch) {
        r.second++;
        dis++;
        while (r.second >= plen[r.first] && r.first < pn) {
            r.second -= plen[r.first];
            r.first++;
        }
    }
    if (dis > 3) {
        eliminated += dis - 1;
        lbound = l;
        rbound = r;
    }
    ...
}

play 函数在寻找相同字符这一部分缺失了其他代码所有的 while(1) 循环, 这导致在进行一次消除后无法进行同组内的连续消除, 进而产生 Wrong Answer.

  • 测例构造思路

aAABBA, 向其中插入 B 后会先发生 BBB 的消除, 但无法进行 AAA 的连续消除, 输出 AAA, 产生Wrong Answer.

  • 构造测例
AABBA
1
2 B

代码 9

  • 错误类型: Runtime Error

  • 错误原因

void play(int rank) {
    ...
    if (eliminated > 0) {
        alen -= eliminated;
        l = lbound;
        r = rbound;
        
        if (l.first >= 0) {
            plen[l.first] = l.second + 1;
        }
        if (r.first < pn) {
            int len = plen[r.first] - r.second;
            if (len > 0) {
                memmove(&p[r.first][0], &p[r.first][r.second], len);
            }
            plen[r.first] = len;
        }
        for (int i = l.first + 1; i < r.first; i++)
            plen[i] = 0;
    }
}

play 函数在执行消除过程中并未考虑特殊处理 l.first = r.first 的情形. 那么 plen[l.first] 被赋值为 l.second + 1, 接下来 len = plen[r.first] - r.second = plen[l.first] - r.second 为负值. 而 plen[r.first]size_t 类型, 即 unsigned long long 类型, 从而在赋值 plen[r.first] = len 时会因下溢出而被赋为很大的正数.

void p2a() {
    int copied = 0;
    for (int i = 0; i < pn; i++) {
        memcpy(&a[copied], p[i], plen[i]);
        copied += plen[i];
    }
}

此时对 a[copied] 进行 memcpy 时则会因为 plen[i] 过大而导致越界访问到 p[i] 长度之外的内存, 产生 Runtime Error.

  • 测例构造思路

只需构造一个在同一数组内发生一次消除, 产生 l.first = r.first 的情形. 此时会因为在 memcpy(&a[copied], p[0], plen[0]) 部分出现越界访问, 产生 Runtime Error.

  • 构造测例
ABBC
1
2 B

代码 10

  • 错误类型: Wrong Answer

  • 错误原因

void play(int rank) {
    ...
    if (eliminated > 0) {
        ...
        for (int i = l.first; i < r.first; i++)
            plen[i] = 0;
        ...
    }
}

play 函数中消除结束后, 如果消除发生在不同的块中, 那么会将 l.first 所在块的长度错误地记为 0, 如果在后续操作中插入珠子的位置在此之后, 那么 find() 函数查找到的位置就会出错, 产生 Wrong Answer.

  • 测例构造思路

输入充分长的串 a 使得 a 被存储在两个数组 p[0], p[1] 中, 随后输出字符使得在 p[0]p[1] 连接处产生消除, 程序将 p[0] 的长度错误地记为 0, 进而之后的插入会插入错误地位置, 直接关联到之后 p2a() 的过程, 产生 Wrong Answer.


文章作者: Chengsx
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Chengsx !
  目录