CCC 2023 J4/S1 Trianglane Solution: Perimeter by Counting Shared Edges



☰

Start NorthStar

CP Weekly Challenge

CCC 2023 J4/S1 Trianglane Solution: Perimeter by Counting Shared Edges

Learn CCC 2023 J4/S1 Trianglane with perimeter counting, shared edges, parity, and O(C) Python and C++ solutions.

CP Weekly Challenge

CCC 2023 J4/S1 Trianglane 题解:从三角形周长到共用边计数

用局部贡献和共用边计数,把三角形巷道周长转化为 O(C) 的线性扫描。

Share this article

Copy the link, open your phone share sheet, scan the QR code for WeChat, or send by email.




Email

If every black triangular tile were isolated, the answer would be immediate: each triangle has three sides, so we would multiply the number of black tiles by three. The only complication is that two adjacent black triangles may share a side. That shared side is counted twice in the initial total, but it needs no warning tape at all.

Trianglane is therefore a counting problem rather than a difficult geometry problem. Start with the full perimeter of every black triangle, then subtract two for every shared edge between black triangles.

Where This Fits in the CCC DSA Map

  • Level: Junior J4 / Senior S1
  • Pattern: Grid Adjacency, Perimeter Counting, Local Contribution
  • Prerequisite: arrays/lists, loops, parity, boundary checks, running totals
  • What this teaches: Break a global geometric quantity into local contributions, then remove the duplicated contribution caused by adjacent cells.
  • Where it appears again: island perimeter, grid edge counting, local contribution methods, graph degree counting

What to Study Next

  • Related CP Weekly problems: CCC 2021 J5/S2 Modern Art for parity counting, CCC 2024 J5 Harvest Waterloo for grid connected components, and CCC 2026 J4 Snail Path for grid simulation.
  • Related Python / C++ foundations: one-dimensional lists/vectors, loops, % 2 parity checks, boundary checks, and running totals.
  • Related DSA roadmap article: CCC Grid Problems: from local adjacency to graph search (planned).

What the Problem Is Asking

The laneway contains two rows of equilateral triangular tiles, with C tiles in each row. Every side is one metre long. Tile orientation alternates: the first tile points upward, and every adjacent tile points in the opposite direction.

The input uses 1 for a black wet tile and 0 for a white dry tile. Bocchi needs warning tape around the outer perimeter of every wet area.

If two black triangles share a complete side, that side is inside a wet area and needs no tape. If two triangles meet only at a vertex, they are not adjacent and their perimeters do not change.

The largest official bound is C <= 200000, so we need a linear-time solution.

Where Beginners Often Get Stuck

The first trap is subtracting only one for a shared edge. Two isolated triangles contribute six sides in total. Their shared side appears once in each triangle's contribution, so it was counted twice. Since neither copy belongs to the outside perimeter, one shared edge reduces the answer by two.

The second trap is cross-row adjacency. Horizontally adjacent tiles in the same row can share a side. Between the two rows, however, a shared side is possible only at zero-based even columns: 0, 2, 4, .... At odd columns, the two tiles meet only at a point.

The third trap is double counting. If every tile checks both its left and right neighbor, the same horizontal shared edge is found twice. Checking only each pair (i, i + 1) counts every horizontal edge exactly once.

Core Algorithm Idea

Let:

B = number of black triangles
E = number of shared edges between two black triangles

If all black triangles were separate, their total perimeter would be 3B. Every shared edge is included twice in that total but contributes nothing to the outside perimeter. Therefore:

answer = 3B - 2E

This is a local-contribution method: add the complete contribution of each unit first, then remove the internal duplication created by adjacency.

We count E in three places:

  1. Adjacent black pairs in the top row.
  2. Adjacent black pairs in the bottom row.
  3. Black pairs across the two rows at zero-based even columns.

Algorithm Steps

  1. Read C, top, and bottom.
  2. Compute B = sum(top) + sum(bottom).
  3. Initialize tape = 3 * B.
  4. For every i from 0 to C - 2, subtract two if top[i] and top[i+1] are both black.
  5. Do the same for the bottom row.
  6. For i = 0, 2, 4, ..., subtract two if top[i] and bottom[i] are both black.
  7. Print tape.

In the second official sample, there are five black triangles, so the initial perimeter is 15. There are two shared black edges, giving 15 - 2 * 2 = 11.

Python Solution

C = int(input())
top = list(map(int, input().split()))
bottom = list(map(int, input().split()))

black_tiles = sum(top) + sum(bottom)
tape = 3 * black_tiles

for i in range(C - 1):
    if top[i] == 1 and top[i + 1] == 1:
        tape -= 2
    if bottom[i] == 1 and bottom[i + 1] == 1:
        tape -= 2

for i in range(0, C, 2):
    if top[i] == 1 and bottom[i] == 1:
        tape -= 2

print(tape)

The code first adds the full perimeter of every black triangle. It then removes the two types of shared edges: horizontal pairs within a row and vertical pairs at even columns. Each shared edge is examined exactly once.

C++ Solution

#include <iostream>
#include <vector>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int C;
    cin >> C;

    vector<int> top(C), bottom(C);
    int blackTiles = 0;

    for (int i = 0; i < C; i++) {
        cin >> top[i];
        blackTiles += top[i];
    }
    for (int i = 0; i < C; i++) {
        cin >> bottom[i];
        blackTiles += bottom[i];
    }

    int tape = 3 * blackTiles;

    for (int i = 0; i + 1 < C; i++) {
        if (top[i] == 1 && top[i + 1] == 1) {
            tape -= 2;
        }
        if (bottom[i] == 1 && bottom[i + 1] == 1) {
            tape -= 2;
        }
    }

    for (int i = 0; i < C; i += 2) {
        if (top[i] == 1 && bottom[i] == 1) {
            tape -= 2;
        }
    }

    cout << tape << '\n';
    return 0;
}

The C++ version uses the same counting logic. The condition i + 1 < C prevents an out-of-bounds access, and i += 2 visits exactly the columns where cross-row shared edges are possible.

Complexity Analysis

The input and all counting loops scan the two rows a constant number of times:

Time: O(C)
Space: O(C)

This easily handles the official maximum of 200,000 columns.

Common Mistakes

  1. Subtracting one instead of two for a shared edge.
  2. Checking cross-row adjacency in every column instead of only zero-based even columns.
  3. Treating triangles that meet at a vertex as if they shared a side.
  4. Checking both left and right neighbors and subtracting the same horizontal edge twice.
  5. Reading row[i + 1] when i is already the last column.
  6. Guessing the perimeter from the picture without defining a countable invariant.

Review Questions

  1. Why does every black triangle initially contribute three?
  2. Why does one shared black edge reduce the answer by two?
  3. Why can cross-row shared edges occur only at zero-based even columns?
  4. How does the loop count every horizontal shared edge exactly once?
  5. What other grid-perimeter problems can use the same “full contribution minus internal edges” idea?

如果每块黑色三角形都互不接触,答案很好算:每块三角形有 3 条边,直接用黑色格子数乘 3。但两块黑色三角形共用一条边时,这条边在初始周长里被算了两次,实际却完全不需要警示胶带。

因此,Trianglane 真正考察的不是复杂几何,而是把图形转成一个准确的计数公式:先算所有黑色三角形的独立周长,再为每一条黑色共用边减去 2。

本题在 CCC DSA 地图里的位置

  • Level: Junior J4 / Senior S1
  • Pattern: Grid Adjacency, Perimeter Counting, Local Contribution
  • Prerequisite: arrays/lists, loops, parity, boundary checks, running totals
  • What this teaches: 把整体几何量拆成每个单元的初始贡献,再扣除相邻单元造成的重复计数。
  • Where it appears again: island perimeter, grid edge counting, local contribution methods, graph degree counting

下一步推荐

如果你已经理解 Trianglane,可以继续看:

  • 相关 CP Weekly 真题:CCC 2021 J5/S2 Modern Art(奇偶性与计数)、CCC 2024 J5 Harvest Waterloo(grid 连通块)、CCC 2026 J4 Snail Path(grid simulation)
  • 相关 Python / C++ 基础:一维 list/vector、双层循环、% 2 判断奇偶、边界检查、running total
  • 相关 DSA 专题:CCC Grid Problems: from local adjacency to graph search(planned)

题目要我们做什么

一条巷道由两行等边三角形瓷砖组成,每行有 C 块。三角形边长都是 1 米,并且方向交替排列:第一块朝上,之后每一块都与前一块方向相反。

输入中的:

1 = 黑色湿瓷砖,需要围住
0 = 白色干瓷砖,不需要围住

我们要计算所有黑色区域外沿一共需要多少米警示胶带。两块黑色瓷砖如果共用一整条边,那条边位于黑色区域内部,不需要胶带;如果只接触一个顶点,则不算相邻,周长不会减少。

官方最大范围是 C <= 200000,所以算法应该在线性时间内完成。

初学者容易卡在哪里

第一个难点是理解“共用一条边”会让答案减少多少。

两块孤立的黑色三角形原本贡献:

3 + 3 = 6

它们连接后,共用边在 6 中被计算了两次,但实际这条边两侧都不需要胶带,所以每条共用边要减去 2,不是减去 1。

第二个难点是上下两行的连接规律。同行的相邻黑色瓷砖会共用边;跨行时,并不是每一列都共用边。按照题目的交替方向,只有 0-based 偶数列 0, 2, 4, ... 的上下两块三角形可能共用一条边。奇数列的上下瓷砖只在顶点相碰。

第三个难点是重复计算。如果同时检查一个格子的左边和右边,一条同行共用边会被检查两次。最简单的做法是只检查相邻下标对 (i, i + 1),每条同行共用边自然只计算一次。

核心算法概念

设:

B = 黑色三角形总数
E = 两块黑色三角形之间的共用边总数

如果所有黑色三角形都孤立,总周长是:

3B

每条共用边在 3B 中出现两次,但不属于外沿,因此:

answer = 3B - 2E

这个思路叫 local contribution:先给每个单元加入完整贡献,再把相邻造成的内部重复部分扣掉。

数据结构设计

只需要保存两行整数:

top[i] = 上面一行第 i 列是否为黑色
bottom[i] = 下面一行第 i 列是否为黑色

然后分别统计三种信息:

  1. 两行中所有的 1,得到 B。
  2. top[i] 与 top[i + 1] 同时为 1 的次数。
  3. bottom[i] 与 bottom[i + 1] 同时为 1 的次数。
  4. 偶数列中 top[i] 与 bottom[i] 同时为 1 的次数。

后三项加起来就是 E。

算法步骤

  1. 读入 C、top 和 bottom。
  2. 计算 B = sum(top) + sum(bottom)。
  3. 令 tape = 3 * B。
  4. 遍历 i = 0 ... C - 2:如果 top[i] 和 top[i+1] 都是黑色,tape -= 2。
  5. 对 bottom 做同样的同行检查。
  6. 遍历偶数列 i = 0, 2, 4, ...:如果上下两块都是黑色,tape -= 2。
  7. 输出 tape。

例如官方第二个样例共有 5 块黑色三角形,初始周长为 15。其中有两条黑色共用边,因此答案为:

15 - 2 * 2 = 11

Python 解法

C = int(input())
top = list(map(int, input().split()))
bottom = list(map(int, input().split()))

black_tiles = sum(top) + sum(bottom)
tape = 3 * black_tiles

for i in range(C - 1):
    if top[i] == 1 and top[i + 1] == 1:
        tape -= 2
    if bottom[i] == 1 and bottom[i + 1] == 1:
        tape -= 2

for i in range(0, C, 2):
    if top[i] == 1 and bottom[i] == 1:
        tape -= 2

print(tape)

代码先一次性加入所有黑色三角形的周长,再分两类扣除共用边:同行相邻,以及偶数列的跨行相邻。每种共用边都只检查一次。

C++ 解法

#include <iostream>
#include <vector>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int C;
    cin >> C;

    vector<int> top(C), bottom(C);
    int blackTiles = 0;

    for (int i = 0; i < C; i++) {
        cin >> top[i];
        blackTiles += top[i];
    }
    for (int i = 0; i < C; i++) {
        cin >> bottom[i];
        blackTiles += bottom[i];
    }

    int tape = 3 * blackTiles;

    for (int i = 0; i + 1 < C; i++) {
        if (top[i] == 1 && top[i + 1] == 1) {
            tape -= 2;
        }
        if (bottom[i] == 1 && bottom[i + 1] == 1) {
            tape -= 2;
        }
    }

    for (int i = 0; i < C; i += 2) {
        if (top[i] == 1 && bottom[i] == 1) {
            tape -= 2;
        }
    }

    cout << tape << '\n';
    return 0;
}

C++ 版本与 Python 使用完全相同的计数逻辑。i + 1 < C 保证同行检查不会越界,而 i += 2 直接枚举能够跨行共边的偶数列。

复杂度分析

读入和三个计数循环都只线性扫描数组:

Time: O(C)
Space: O(C)

当 C = 200000 时,这个复杂度也足够快。如果边读入边做更复杂的处理,可以把额外空间优化到 O(C) 以下,但本题没有必要牺牲可读性。

常见错误

  1. 每条共用边只减 1;正确值是 2。
  2. 在所有列检查上下相邻;只有 0-based 偶数列会跨行共用边。
  3. 把只接触一个顶点的三角形当成共用边。
  4. 同时向左、向右检查,导致同行共用边重复扣除。
  5. 循环跑到 i = C - 1 后仍访问 i + 1,造成越界。
  6. 先凭图形猜周长,却没有建立可验证的计数公式。

复习问题

  1. 为什么每块黑色三角形一开始贡献 3?
  2. 为什么每条黑色共用边要从答案中减去 2?
  3. 为什么上下两行只在 0-based 偶数列检查共用边?
  4. 如何保证每条同行共用边只被计算一次?
  5. 这个“初始贡献减内部共用边”的方法还能解决哪些 grid perimeter 问题?

Questions or feedback?

Have a question about this article or want to suggest an improvement? Send us a private message.

Send Feedback

有问题或建议?

如果你对本文有疑问,或希望提出改进建议,请给我们发送私密留言。

发送反馈

Back to CP Weekly Challenge
返回 CP Weekly Challenge

CP ACADEMY

Not sure which challenges fit your level?

Use the CP assessment to explore your current starting point and choose topics to practise next. You can keep reading and practising without taking an assessment.

60 minutes continuously · Python or C++ · Choose your competition goal

Discover Your CP Level

Scroll to Top