C#을 통한 Advent of Code 2022 | ANDREA ANGELLA

오 이런 것이 있었군요.

https://adventofcode.com/

1번 부터 25번까지 알고리즘 문제가 열리며 지금은 14번째 문제가 게시되어 있습니다.

ANDREA ANGELLA님이 C#을 이용해 Advent of Code 2022 문제를 풀고 있습니다!


2개의 좋아요

저의 Day1 문제 풀입니다. 문제를 응용할 수 있는 문제가 새로 열립니다. 고전적으로 풀었습니다.

var lines = File.ReadAllLines("day1_input.txt");
var (max, sum) = (0, 0);
foreach (var line in lines)
{
    if (line is "")
    {
        if (sum > max)
            max = sum;
        sum = 0;

        continue;
    }

    sum += int.Parse(line);
}

Console.WriteLine(max);
2개의 좋아요

Day1의 첫 번째 문제를 풀면 응용하는 두 번째 문제가 열리는데 상위 3개의 합을 구하는 것입니다. 슬슬 LINQ로 할껄 약간의 후회를 하게 되네요 ^^;

1개의 좋아요

위의 코드는 다음처럼 LINQ로도 표현 가능합니다.

var max = File.ReadAllText("day1_input.txt")
    .Split(Environment.NewLine + Environment.NewLine)
    .Select(x =>
         x.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries)
        .Sum(y => int.Parse(y))
    )
    .Max();

Console.WriteLine(max);
2개의 좋아요

가독성은? 글쎄요… 첫 번째 코드가 좋아 보입니다.

1개의 좋아요

Day1의 두번째 문제의 풀이입니다. LINQ를 사용했으므로 정렬을 한 후 상위 3개의 합을 구하는 것으로 풀 수 있습니다.

var max = File.ReadAllText("day1_input.txt")
    .Split(Environment.NewLine + Environment.NewLine)
    .Select(x =>
        x.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries)
        .Sum(y => int.Parse(y))
    )
    .OrderByDescending(x => x)
    .Take(3)
    .Sum();

Console.WriteLine(max);
2개의 좋아요

Day2 문제를 풀어보았습니다.
이해하면 간단한 문제인데 영어라 처음 이해하기가 어려웠네요.

다음은 풀이입니다.

    internal class Day2
    {
        /// <summary>
        /// 가위바위보 게임에서 승리하자
        /// </summary>
        public static void Solve1()
        {
            var lines = File.ReadAllLines("day2_input.txt");

            var sum = 0;
            foreach (var line in lines)
            {
                var temp = line.Split();
                var y = temp[0] switch { "A" => Kind.바위, "B" => Kind.보, "C" => Kind.가위, _ => throw new InvalidOperationException() };
                var i = temp[1] switch { "X" => Kind.바위, "Y" => Kind.보, "Z" => Kind.가위, _ => throw new InvalidOperationException() };

                var score = (int)i;

                // 이긴 경우
                if ((i, y) is (Kind.바위, Kind.가위) ||
                    (i, y) is (Kind.보, Kind.바위) ||
                    (i, y) is (Kind.가위, Kind.보))
                    score += 6;
                // 비긴 경우
                else if (i == y)
                    score += 3;

                sum += score;
            }

            Console.WriteLine(sum);
        }
    }

    internal enum  Kind
    {
        바위 = 1,
        보 = 2,
        가위 = 3
    }
1개의 좋아요

만들고 나니 영 마음에 들지 않습니다.

이긴 경우에 대한 패턴을 좀 더 분석한 후 다음의 코드를 생성하였습니다.

var lines = File.ReadAllLines("day2_input.txt");

var sum = 0;
foreach (var line in lines)
{
    var y = line[0] - 'A';
    var i = line[2] - 'X';

    var score = 0;
    // 비긴경우
    if (i == y)
        score = 3;
    // 이긴경우
    else if (i == (y + 1) % 3)
        score = 6;

    sum += score + i + 1;
}

Console.WriteLine(sum);
2개의 좋아요

Day2의 심화 문제도 비슷한 패턴으로 풀 수 있습니다.

var lines = File.ReadAllLines("day2_input.txt");

var sum = 0;
foreach (var line in lines)
{
    var y = line[0] - 'A';  // 0:바위, 1:보, 2:가위
    var r = line[2] - 'X';  // 0:짐, 1:비김, 2:이김

    // 0, 0 : 바위, i = 2
    // 0, 1 : 바위, i = 0
    // 0, 2 : 바위, i = 1
    // 1, 0 : 보, i = 0
    // 1, 1 : 보, i = 1
    // 1, 2 : 보, i = 2
    // 2, 0 : 가위, i = 1
    // 2, 1 : 가위, i = 2
    // 2, 2 : 가위, i = 0

    var i = (y + r + 2) % 3;
    sum += r * 3 + i + 1;
}

Console.WriteLine(sum);

그런데 코드는 간결해 졌지만 가독성은 떨어지는군요.

2개의 좋아요

Day3 문제를 풀어봤습니다.

vJrwpWtwJgWrhcsFMMfFFhFp
jqHRNqRjqzjGDLGLrsFMfFZSrLrFZsSL
PmmdzqPrVvPwwTWBwg
wMqvLMZHhHMvwLHjbvcjnnSBnvTQFn
ttgJtRGJQctTZtZT
CrZsJsPPZsGzwwsLwLmpwMDw

각 문자열을 절반으로 자른 후 왼쪽 문자열과 오른쪽 문자열 중 같은 첫 문자를 우선순위 값으로 치환해서 합계를 구하면 풀 수 있습니다.

var lines = File.ReadAllLines("day3_input.txt");
//var lines = new[]
//{
//    "vJrwpWtwJgWrhcsFMMfFFhFp",
//    "jqHRNqRjqzjGDLGLrsFMfFZSrLrFZsSL",
//    "PmmdzqPrVvPwwTWBwg",
//    "wMqvLMZHhHMvwLHjbvcjnnSBnvTQFn",
//    "ttgJtRGJQctTZtZT",
//    "CrZsJsPPZsGzwwsLwLmpwMDw",
//};
var sum = 0;
foreach (var line in lines)
{
    var rucksackSize = line.Length / 2;
    var aList = line[..rucksackSize];
    var bList = line[rucksackSize..];

    //var result = from a in aList
    //             join b in bList
    //             on a equals b
    //             select a;
    //var c = result.FirstOrDefault();
    var c = aList
            .Join(bList, x => x, y => y, (x, y) => x)
            .FirstOrDefault();
    var priority = GetPriorityNumber(c);
    sum += priority;
}

Console.WriteLine(sum);

static int GetPriorityNumber(char c) => c switch
{
    >= 'a' and <= 'z' => c - 'a' + 1,
    >= 'A' and <= 'Z' => c - 'A' + 27,
    _ => 0
};

LINQ를 사용하면 쉽게 풀리는 문제네요.

1개의 좋아요

Join() 대신 Intersect()로 좀 더 간단히 표현할 수 있습니다.

                var c = aList
                    .Intersect(bList)
                    .FirstOrDefault();
1개의 좋아요

Day3의 심화 문제입니다. 심화 문제는 3줄에 공통으로 들어간 문자를 찾아 우선순위 값으로 변환한 후 그 합계를 구하는 것입니다.

이런 문제는 LINQ를 이용하면 쉽게 풀립니다.

var lines = File.ReadAllLines("day3_input.txt");
//var lines = new[]
//{
//    "vJrwpWtwJgWrhcsFMMfFFhFp",
//    "jqHRNqRjqzjGDLGLrsFMfFZSrLrFZsSL",
//    "PmmdzqPrVvPwwTWBwg",
//    "wMqvLMZHhHMvwLHjbvcjnnSBnvTQFn",
//    "ttgJtRGJQctTZtZT",
//    "CrZsJsPPZsGzwwsLwLmpwMDw",
//};

var sum = lines.Chunk(3)
    .Select(x => x[0].Intersect(x[1]).Intersect(x[2]).FirstOrDefault())
    .Sum(x => GetPriorityNumber(x));

Console.WriteLine(sum);


static int GetPriorityNumber(char c) => c switch
{
    >= 'a' and <= 'z' => c - 'a' + 1,
    >= 'A' and <= 'Z' => c - 'A' + 27,
    _ => 0
};
1개의 좋아요

Day4의 문제는 두 개의 범위 중 포함되는 범위의 개수를 구하는 것입니다.

public static void Solve1()
{
var lines = File.ReadAllLines("day4_input.txt");
//var lines = new[]
//{
//    "2-4,6-8",
//    "2-3,4-5",
//    "5-7,7-9",
//    "2-8,3-7",
//    "6-6,4-6",
//    "2-6,4-8",
//};

var count = lines.Select(x => x.Split(','))
    .Select(x => new
    {
        A = x[0].Split('-').Select(y => int.Parse(y)).ToArray(),
        B = x[1].Split('-').Select(y => int.Parse(y)).ToArray()
    })
    .Count(x => IsInclude(x.A, x.B));

Console.WriteLine(count);


static bool IsInclude(int[] r1, int[] r2) => (r1, r2) switch
{
    _ when r1[0] <= r2[0] && r1[1] >= r2[1] => true,
    _ when r2[0] <= r1[0] && r2[1] >= r1[1] => true,
    _ => false
};

결과가 나오면 평균은 하는 것이겠지만 ANDREA ANGELLA님의 풀이도 궁금해지는군요.

var assignments = File.ReadAllLines("Input.txt")
                          .Select(pair => pair.Split(","))
                          .Select(x => (Assignment.Parse(x[0]), Assignment.Parse(x[1])));

var fullyContainedAssignments = assignments.Where(a => a.First.FullyContains(a.Second) || a.Second.FullyContains(a.First));

Assert.That(fullyContainedAssignments.Count(), Is.EqualTo(518));

private record Assignment(int Start, int End)
{
    public bool Overlap(Assignment a) => End < a.Start || Start > a.End;
    public bool FullyContains(Assignment a) => Start <= a.Start && End >= a.End;
    public static Assignment Parse(string a) => new(int.Parse(a.Split("-")[0]), int.Parse(a.Split("-")[1]));
}

깔끔하군요.

1개의 좋아요

Day4의 응용문제는 두개의 범위가 겹칠 경우를 카운트 하는 것입니다. 다양한 풀이가 있겠지만 이번에는 좀 더 무식하게 짜봤습니다.

public static void Solve2()
{
    var lines = File.ReadAllLines("day4_input.txt");
    //var lines = new[]
    //{
    //    "2-4,6-8",
    //    "2-3,4-5",
    //    "5-7,7-9",
    //    "2-8,3-7",
    //    "6-6,4-6",
    //    "2-6,4-8",
    //};

    var count = lines.Select(x => x.Split(','))
        .Select(x => new
        {
            A = x[0].Split('-').Select(y => int.Parse(y)).ToArray(),
            B = x[1].Split('-').Select(y => int.Parse(y)).ToArray()
        })
        .Count(x => IsOverlap(x.A, x.B));

    Console.WriteLine(count);


    static bool IsOverlap(int[] r1, int[] r2)
    {
        var r1List = Enumerable.Range(r1[0], r1[1] - r1[0] + 1);
        var r2List = Enumerable.Range(r2[0], r2[1] - r2[0] + 1);

        return r1List.Intersect(r2List).Any();
    }    
}
1개의 좋아요

Day5 문제는 텍스트 정보에서 스택 목록을 만들고 이동 명령을 해석해서 스택을 조정한 뒤 스택에 쌓인 가장 위의 물건을 얻는 문제입니다.

    [D]    
[N] [C]    
[Z] [M] [P]
 1   2   3 

move 1 from 2 to 1
move 3 from 1 to 3
move 2 from 2 to 1
move 1 from 1 to 2

최대한 LINQ로 풀려고 노력해봤고 아는 LINQ 명령이 많지 않다는 것을 느꼈습니다. 저에게 AoC 문제는 LINQ에 좀 더 익숙해질 수 있는 경험이 되곘군요.

public static void Solve1()
{
    var lines = File.ReadAllText("day5_input.txt");
    //var lines = """
    //        [D]    
    //    [N] [C]    
    //    [Z] [M] [P]
    //     1   2   3 

    //    move 1 from 2 to 1
    //    move 3 from 1 to 3
    //    move 2 from 2 to 1
    //    move 1 from 1 to 2
    //    """;

    // 스택부와 명령부 라인 얻음
    var (stacksLines, commandLines) = lines
        .Split(Environment.NewLine + Environment.NewLine)
        .Chunk(2)
        .Select(x =>
            (x[0].Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries),
            x[1].Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries)))
        .First();

    // 스택부에서 박스 정보 얻음
    var stackBoxes = stacksLines
        .Select(x => x
            .Chunk(4)
            .Select((y, i) => (Number: i + 1, Box: y[1]))
            .Where(x => x.Box is not ' ') // 빈 슬롯 제외
        )
        .SkipLast(1) // 마지막 숫자부 제외
        .SelectMany(x => x)
        .Reverse(); // 아래부터 담아야 하므로 반전

    var stackCount = stackBoxes.Max(x => x.Number);

    // 박스를 스택에 담음
    var stacks = new Stack<char>[stackCount];
    foreach (var stackBox in stackBoxes)
    {
        ref var stack = ref stacks[stackBox.Number - 1];
        if (stack is null)
            stack = new Stack<char>();

        stack.Push(stackBox.Box);
    }

    // 명령부 처리
    foreach (var commandLine in commandLines)
    {
        var (moves, from, to) = commandLine.Split(' ')
            .Chunk(6)
            .Select(x => (int.Parse(x[1]), int.Parse(x[3]), int.Parse(x[5])))
            .First();

        foreach (var count in Enumerable.Range(1, moves))
        {
            var stack = stacks[from - 1].Pop();
            stacks[to - 1].Push(stack);
        }
    }

    var result = string.Concat(stacks.Select(x => x.First().ToString()));
    Console.WriteLine(result);
}

문제를 계속 풀어가다 보면 문제 해결을 위해 문제를 연속된 정보 - Stream으로 보고 풀어가는 문제 해결 방법에 대해 좀 더 익숙해지지 않을까 생각해봅니다.

1개의 좋아요

Day5의 심화 문제입니다. 이제는 상자를 하나씩 옮기는 것이 아니라 옮겨야 하는 상자를 동시에 옮겼을 때 최종 구성된 스택에서 상단에 놓진 박스들을 구하는 문제입니다.

더이상 스택이 필요 없으므로 여러개를 옮길 수 있는 List<T>로 컬렉션을 변경하고 명령 처리 후 최종 스택 상단의 박스를 결합하는 것을 string.Concat() 대신 LINQ Aggregate()를 사용하였습니다.

public static void Solve2()
{
    var lines = File.ReadAllText("day5_input.txt");
    //var lines = """
    //        [D]    
    //    [N] [C]    
    //    [Z] [M] [P]
    //     1   2   3 

    //    move 1 from 2 to 1
    //    move 3 from 1 to 3
    //    move 2 from 2 to 1
    //    move 1 from 1 to 2
    //    """;

    // 스택부와 명령부 라인 얻음
    var (stacksLines, commandLines) = lines
        .Split(Environment.NewLine + Environment.NewLine)
        .Chunk(2)
        .Select(x =>
            (x[0].Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries),
            x[1].Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries)))
        .First();

    // 스택부에서 박스 정보 얻음
    var stackBoxes = stacksLines
        .Select(x => x
            .Chunk(4)
            .Select((y, i) => (Number: i + 1, Box: y[1]))
            .Where(x => x.Box is not ' ') // 빈 슬롯 제외
        )
        .SkipLast(1) // 마지막 숫자부 제외
        .SelectMany(x => x);

    var stackCount = stackBoxes.Max(x => x.Number);

    // 박스를 스택에 담음
    var stacks = new List<char>[stackCount];
    foreach (var stackBox in stackBoxes)
    {
        ref var stack = ref stacks[stackBox.Number - 1];
        if (stack is null)
            stack = new List<char>();

        stack.Add(stackBox.Box);
    }

    // 명령부 처리
    foreach (var commandLine in commandLines)
    {
        var (moves, from, to) = commandLine.Split(' ')
            .Chunk(6)
            .Select(x => (int.Parse(x[1]), int.Parse(x[3]), int.Parse(x[5])))
            .First();

        var fromStacks = stacks[from - 1];
        var toStacks = stacks[to - 1];

        var movingBoxes = fromStacks.GetRange(0, moves);
        fromStacks.RemoveRange(0, moves);
        toStacks.InsertRange(0, movingBoxes);
    }

    //var result = string.Concat(stacks.Select(x => x.First().ToString()));
    var result = stacks.Aggregate("", (x, stack) => x + stack.First());
    Console.WriteLine(result);
}

다음 문제부터는 문제가 유도하는 취지에 맞게 유닛 테스트로 구성해서 해봐야겠습니다.