AoC 2022 - slog

Advent Of Code 2022 퍼즐을 완료하기 위한 슬로그를 시작합니다.

목적은 다음과 같습니다.

  • 풀이에 적합한 LINQ 사용법 숙달
  • 풀이 후 다른 분의 풀이를 살펴보며 좀 더 효과적인 코드 습득

참고 풀이

2개의 좋아요

이전 풀이 이력

1개의 좋아요

AoC 문제의 흥미로운 특징은 문제의 표현이 개조식이 아닌 서술식 이며 스토리가 있어서 마치 우리가 풀어야 하는 일상 생활의 문제처럼 보이게 합니다. 스토리가 있어서 어떤 면에서는 불필요해 보이는 문장도 있어 보입니다. 그럼에도 불구하고 예시와 그 답을 알려줘서 제대로 풀고 있는지 검토 할 수 있도록 합니다.

이렇게 문제를 내는 이유는 아마도 일상생활에서 해결해야 할 문제를 재현하기 위한 것일지 모릅니다. 우리는 그 안에서 패턴을 찾고 패턴의 규칙성에서 공식을 찾은 후 그 공식으로 코드로 구현하기 됩니다.

1개의 좋아요

Day6

문자열 중 연속된 4개의 문자가 중복되지 않는 위치를 찾는 문제입니다.

“bvwbjplbgvbhsrlpgdmjqwftvncz”

  • bvwb 에서 b가 중복되므로 다음으로 넘어갑니다.
  • vwbj 는 중복되지 않으므로 j의 위치인 5가 답입니다.

“nppdvjthqldpwncqszvftbrmjlhg”

  • nppd는 p 중복
  • ppdv p 중복
  • pdvj 중복되지 않으므로 j의 위치인 6이 답입니다.

다음은 풀이입니다.

public static string Solve1(string input)
{
    for (var i = 0; i < input.Length; i++)
    {
        // 총 4개의 문자가 준비될 때까지 처리하지 않음
        if (i < 4)
            continue;

        var packet = input[(i - 4)..i];
        if (IsMarker(packet) is true)
            return i.ToString();
    }

    return (-1).ToString();

    bool IsMarker(string packet)
    {
        return packet.Distinct().SequenceEqual(packet);
    }
}

심화 문제

패킷 시작 마커가 아닌 메시지 패킷 마커를 찻는 문제로 4개가 아닌 14개의 연속된 개별문자여야 합니다. 위의 코드에서 약간의 수정으로 답을 알 수 있습니다.

public static string Solve2(string input)
{
    for (var i = 0; i < input.Length; i++)
    {
        // 총 14개의 문자가 준비될 때까지 처리하지 않음
        if (i < 14)
            continue;

        var packet = input[(i - 14)..i];
        if (IsMarker(packet) is true)
            return i.ToString();
    }

    return (-1).ToString();

    bool IsMarker(string packet)
    {
        return packet.Distinct().SequenceEqual(packet);
    }
}
1개의 좋아요

다른 분들의 풀이가 궁금합니다.

Andrea Angella님의 풀이입니다. 저랑 같은 풀이와 한가지 더 a-z 만큼의 int 배열을 만들고 각 자리가 발생할 때 값을 증가시킴으로써 마커에 중복된 문자가 있는지를 체크하는 코드도 확인할 수 있습니다.

    [TestCase(4, 1578)]
    [TestCase(14, 2178)]
    public void Test(int n, int expected)
    {
        var input = File.ReadAllText("Input.txt");

        for (var i = n; i < input.Length; i++)
        {
            var sequence = input[(i - n)..i];

            if (sequence.Distinct().SequenceEqual(sequence))
            {
                Assert.That(i, Is.EqualTo(expected));
                return;
            }
        }

        Assert.Fail();
    }

    [TestCase(4, 1578)]
    [TestCase(14, 2178)]
    public void TestWithArray(int n, int expected)
    {
        var letters = new int[26];
        var input = File.ReadAllText("Input.txt");

        for (var i = 0; i < input.Length; i++)
        {
            if (i >= n) letters[input[i-n]-'a']--;
            letters[input[i]-'a']++;

            if (letters.Sum() == n && letters.All(x => x <= 1))
            {
                Assert.That(i + 1, Is.EqualTo(expected));
                return;
            }
        }

        Assert.Fail();
    }

Sound Code의 Mark Heath님의 코드는 사뭇 다릅니다.

    public (string, string) ExpectedResult => ("1892", "2313");

    public int FindMarkerPosition(string input, int distinct) =>
        input.Window(distinct).TakeUntil(w => w.ToHashSet().Count == distinct).Count() + distinct - 1;

    public (string, string) Solve(string[] input)
    {
        return ($"{Part1(input)}", $"{Part2(input)}");
    }

    long Part1(IEnumerable<string> input) => FindMarkerPosition(input.First(), 4);
    long Part2(IEnumerable<string> input) => FindMarkerPosition(input.First(), 14);

SuperLinq를 사용해서 완전한 LINQ 스타일로 문제를 푸셨네요.

1개의 좋아요

Mark Heath님의 코드를 이해하기 위해 SuperLINQ(MoreLINQ로도 가능)를 이용해 코드를 재작성하였습니다.

public static string Solve1_LINQ(string input)
{
    var markerSize = 4;
    var result = input.Window(markerSize)
        .TakeUntil(x => x.ToHashSet().Count == markerSize)
        .Count() + markerSize - 1;

    return result.ToString();
}
  • Window(windowSize)는 윈도우 사이즈 만큼 목록을 순회할 수 있게 합니다.
    예를 들어 목록이 [‘a’, ‘b’, ‘c’, ‘d’, ‘e’, ‘f’]일 경우 윈도우 사이즈가 3일 때,
    [‘a’, ‘b’, ‘c’],
    [‘b’, ‘c’, ‘d’],
    [‘c’, ‘d’, ‘e’],
    [‘d’, ‘e’, ‘f’]
    이 됩니다.
  • TakeUntil()는 조건이 참일때까지 목록을 얻습니다.
  • ToHashSet()으로 중복되지 않는 집합을 만듭니다.
1개의 좋아요

@dimohy 님 슬로그를 보고 저도 따라서 풀어보고 있습니다. 매 년 12월을 심심하지 않게 보낼 수 있겠네요. 좋은 정보 고맙습니다~

2개의 좋아요

Day 7

주어진 명령어와 처리 결과를 통해 100000 보다 작은 디렉토리 사이즈의 총합을 구하는 문제입니다.

$ cd /
$ ls
dir a
14848514 b.txt
8504156 c.dat
dir d
$ cd a
$ ls
dir e
29116 f
2557 g
62596 h.lst
$ cd e
$ ls
584 i
$ cd ..
$ cd ..
$ cd d
$ ls
4060174 j
8033020 d.log
5626152 d.ext
7214296 k

제 코딩 실력으로는 짧은 코드가 나오지 않는데요,

public static string Solve1(string input)
{
    var lines = input.Split(Environment.NewLine);
    var root = Parse(lines);

    var result = root.SearchDirectories()
        .Where(x => x.Size < 100000)
        .Sum(x => x.Size);
    return result.ToString();
}

static DirectoryNode Parse(string[] lines)
{
    var root = new DirectoryNode(null, "/");
    DirectoryNode? currentDirectory = null;
    foreach (var line in lines)
    {
        // 명령어의 경우 명령 처리
        if (line.StartsWith("$") is true)
        {
            var tokens = line.Split(' ');
            if (tokens[1] is "cd")
            {
                if (tokens[2] is "/")
                    currentDirectory = root;
                else if (tokens[2] is "..")
                {
                    currentDirectory = currentDirectory?.Parent;
                }
                else
                {
                    var dictionaryName = tokens[2];
                    currentDirectory = currentDirectory?.Nodes.FirstOrDefault(x => x is DirectoryNode && x.Name == dictionaryName) as DirectoryNode;
                }
            }
            else if (tokens[1] is "ls")
                continue;
            else
            {
                throw new InvalidOperationException();
            }
        }
        // 아닌 경우 파일 사이즈 취합
        else
        {
            var tokens = line.Split(' ');

            // 디렉토리
            if (tokens[0] is "dir")
            {
                var newDirectory = new DirectoryNode(currentDirectory, tokens[1]);
                currentDirectory?.AddNode(newDirectory);
            }
            // 파일
            else
            {
                var newFile = new FileNode(tokens[1], long.Parse(tokens[0]));
                currentDirectory?.AddNode(newFile);
            }
        }
    }

    return root;
}


abstract record Node(string Name)
{
    public abstract long Size { get; }
}
record DirectoryNode(DirectoryNode? Parent, string Name) : Node(Name)
{
    public IEnumerable<Node> Nodes { get; } = new List<Node>();

    public override long Size
    {
        get
        {
            var totals = 0L;
            foreach (var node in Nodes)
                totals += node.Size;
            return totals;
        }
    }

    public IEnumerable<DirectoryNode> SearchDirectories()
    {
        foreach (var node in Nodes)
        {
            if (node is DirectoryNode directory)
            {
                yield return directory;
                foreach (var subDirectory in directory.SearchDirectories())
                    yield return subDirectory;
            }
        }
    }

    public void AddNode(Node node) => (Nodes as List<Node>)!.Add(node);
}
record FileNode(string Name, long FileSize) : Node(Name)
{
    public override long Size => FileSize;
}

심화 문제

파일시스템의 총 사이즈는 70000000이고 남은 용량이 30000000이 필요합니다.
한 디렉토리를 삭제 했을 때 필요한 남은 용량을 확보하기 위한 문제입니다. 기준에 부합하면서 그중에 가장 작은 디렉토리의 사이즈를 구하면 됩니다.

public static string Solve2(string input)
{
    var lines = input.Split(Environment.NewLine);
    var root = Parse(lines);

    var usedSize = root.Size;
    var mustDeleteSize = 30000000 - (70000000 - usedSize);

    var result = root.SearchDirectories()
        .Where(x => x.Size > mustDeleteSize)
        .OrderBy(x => x.Size)
        .Select(x => x.Size)
        .First();
    return result.ToString();
}

다른 분의 코드를 살펴볼 필요가 있어 보입니다.

1개의 좋아요

안젤라 님은 저랑 유사하게 짰습니다.

마크히스님도… 유사합니다.

1개의 좋아요
public IEnumerable<DirectoryNode> SearchDirectories()
{
    foreach (var node in Nodes)
    {
        if (node is DirectoryNode directory)
        {
            yield return directory;
            foreach (var subDirectory in directory.SearchDirectories())
                yield return subDirectory;
        }
    }
}

이 코드를 개선하고 싶은데요, 당장은 딱히 아이디어가 떠오르지 않습니다.

1개의 좋아요
public IEnumerable<DirectoryNode> SearchDirectories()
{
    foreach (var node in Nodes)
    {
        if (node is DirectoryNode directory)
        {
            yield return directory;
            foreach (var subDirectory in directory.SearchDirectories())
                yield return subDirectory;
        }
    }
}

이 부분에 대한 답글을 남겨야겠다라고 생각하며 읽고 있었는데, 역시 같은 생각이었네요 ㅎㅎ

이전 글에서 LINQ로 풀고 싶다고 하셔서 저는 이렇게 해보았습니다

public IEnumerable<DirectoryNode> SearchDirectories()
{
    return Nodes.OfType<DirectoryNode>()
                .SelectMany(directoryNode => directoryNode.SearchDirectories());
}
2개의 좋아요

굿입니다. 답은 그곳에 있었군요!

2개의 좋아요

Day 8

30373
25512
65332
33549
35390

숫자는 한자리의 좌표이며 나무의 높이를 의미합니다. 0은 가장 낮고 9가 가장 높은 나무입니다.

일반: 존 밖에서 상, 하, 좌, 우 이 지역을 관찰하면서 보이는 나무의 개수를 구하는 문제입니다.
심화: 경치가 좋은 위치를 구하는 문제입니다. 지점에서 상, 하, 좌, 우의 나무가 많이 보일 수록 좋은 위치이며 점수는 보이는 나무 갯수만끔 (상 나무 * 하 나무 * 좌 나무 * 우 나무)가 됩니다.

저는 세련되게 만드는 방법이 바로 떠오르지 않아 일반적으로 코딩을 했습니다. 다른 분들의 코드가 궁금한 시점이군요.

public static string Solve1(string input)
{
    var map = MakeMap(input);
    var check = new bool[map.GetLength(0), map.GetLength(1)];

    for (var y = 0; y < map.GetLength(1); y++)
        ScanH(y);
    for (var x = 0; x < map.GetLength(0); x++)
        ScanV(x);

    var sum = 0;
    for (var y = 0; y < check.GetLength(1); y++)
        for (var x = 0; x < check.GetLength(0); x++)
            sum += check[x, y] is true ? 1 : 0;

    return sum.ToString();

    void ScanH(int y)
    {
        var high1 = -1;
        var high2 = -1;
        var length = map.GetLength(0);
        for (var x = 0; x < length; x++)
        {
            if (map[x, y] > high1)
            {
                high1 = map[x, y];
                check[x, y] = true;
            }
            if (map[length - x - 1, y] > high2)
            {
                high2 = map[length - x - 1, y];
                check[length - x - 1, y] = true;
            }
        }
    }

    void ScanV(int x)
    {
        var high1 = -1;
        var high2 = -1;
        var length = map.GetLength(1);
        for (var y = 0; y < length; y++)
        {
            if (map[x, y] > high1)
            {
                high1 = map[x, y];
                check[x, y] = true;
            }
            if (map[x, length - y - 1] > high2)
            {
                high2 = map[x, length - y - 1];
                check[x, length - y - 1] = true;
            }
        }
    }
}

static int[,] MakeMap(string input)
{
    var lines = input.Split(Environment.NewLine);

    var map = new int[lines[0].Length, lines.Length];
    for (var y = 0; y < lines.Length; y++)
    {
        var line = lines[y];
        for (var x = 0; x < line.Length; x++)
        {
            map[x, y] = int.Parse(line[x].ToString());
        }
    }

    return map;
}

public static string Solve2(string input)
{
    var map = MakeMap(input);

    var maxScore = 0;
    for (var y = 0; y < map.GetLength(1); y++)
    {
        for (var x = 0; x < map.GetLength(0); x++)
        {
            var score = ScanScore(x, y);
            if (score > maxScore)
                maxScore = score;
        }
    }

    return maxScore.ToString();

    int ScanScore(int x, int y)
    {
        var xLength = map.GetLength(0);
        var yLength = map.GetLength(1);

        var score = 1;
        var center = map[x, y];

        // 왼쪽 탐색
        var (sx, sy) = (x - 1, y);
        var high = -1;
        while (sx >= 0)
        {
            var another = map[sx, sy];
            if (another > center && another <= high)
                break;
            if (another < center && center <= high)
                break;

            if (another > high)
                high = another;

            sx--;
        }
        score *= x - (sx + 1);

        // 오른쪽 탐색
        (sx, sy) = (x + 1, y);
        high = -1;
        while (sx < xLength)
        {
            var another = map[sx, sy];
            if (another > center && another <= high)
                break;
            if (another < center && center <= high)
                break;

            if (another > high)
                high = another;

            sx++;
        }
        score *= (sx - 1) - x;

        // 위 탐색
        (sx, sy) = (x, y - 1);
        high = -1;
        while (sy >= 0)
        {
            var another = map[sx, sy];
            if (another > center && another <= high)
                break;
            if (another < center && center <= high)
                break;

            if (another > high)
                high = another;

            sy--;
        }
        score *= y - (sy + 1);

        // 아래 탐색
        (sx, sy) = (x, y + 1);
        high = -1;
        while (sy < yLength)
        {
            var another = map[sx, sy];
            if (another > center && another <= high)
                break;
            if (another < center && center <= high)
                break;

            if (another > high)
                high = another;

            sy++;
        }
        score *= (sy - 1) - y;

        return score;
    }
}
1개의 좋아요

흠 안젤라님의 풀이는 저랑 사뭇 다르군요. 배울 점은, LINQ로 2차원 배열을 만들었다는 것. (만들 수 있다는 것!)

마크히스의 풀이는 놀랄 노자이군요. LINQ 스타일로 문제를 풀었습니다. 코드가 매우 간결하고 코드를 이해할 수만 있다면 가장 아름다운 코드인 것 같습니다.

public class Day8 : ISolver
{
    public (string, string) ExpectedResult => ("1832", "157320");

    private static readonly Coord[] dirs = new Coord[] { (0, 1), (1, 0), (0, -1), (-1, 0) };

    public (string, string) Solve(string[] input)
    {
        var g = Grid<int>.ParseToGrid(input, c => c - '0');        
        return ($"{Part1(g)}", $"{Part2(g)}");
    }

    private long Part1(Grid<int> g)
            => g.AllPositions().Count(p =>  dirs.Any(d => g.LineOut(p,d).All(n => g[p] > g[n])));


    public long ScenicScore(Grid<int> g, Coord p)
        => dirs.Select(d => g.LineOut(p, d)
                            .TakeUntil(n => g[p] <= g[n]).Count())
               .Aggregate((a, b) => a * b);

    public long Part2(Grid<int> g)
        => g.AllPositions().Max(p => ScenicScore(g, p));
}

마크히스님은 좌표계를 처리하기 위한 Grid<T> 클래스를 만들어서 이후 이러한 문제를 푸는 것 같습니다.

1개의 좋아요

2차원 탐색할 때 이렇게 하면 코드가 간결해져서 많이 사용하는 방식이죠~
오랜만에 보니 반갑네요 :slight_smile:

2개의 좋아요

Day9

갈수록 문제가 어려워지는군요…

일반: 매듭이 이동할 때 꼬리의 경로를 구해 그 경로 위치의 합을 구하는 문제입니다. 매듭의 길이는 2입니다.
심화: 이제 매듭의 길이가 10이 됩니다. 규칙에 의해 이동된 매듭의 마지막 경로를 구해 그 위치의 합을 구하는 문제입니다.

    public static string Solve1(string input)
    {
        // 입력에서 헤드의 이동 얻기
        var headPoints = input
            .Split(Environment.NewLine)
            .Select(x => ParseHeadMove(x))
            .SelectMany(x => x)
            .Aggregate(Enumerable.Empty<Point>().Append(new Point(0, 0)), (items, item) => items.Append(items.Last() + item)) // 좀 더 적절한 LINQ 함수 찾아야 함
            .ToArray();

        var tailPoints = new List<Point>() { new Point(0, 0) };
        Point beforeHeadPoint = headPoints.First();
        foreach (var headPoint in headPoints.Skip(1))
        {
            var lastTailPoint = tailPoints.Last();

            if (lastTailPoint.IsAdjacency(headPoint) is false)
                tailPoints.Add(beforeHeadPoint);

            beforeHeadPoint = headPoint;
        }

        return tailPoints.Distinct().Count().ToString();
    }

    static IEnumerable<Point> ParseHeadMove(string command)
    {
        var (cmd, move) = command.Split(' ')
            .Chunk(2)
            .Select(x => (x[0], int.Parse(x[1])))
            .First();

        var moving = cmd switch
        {
            "L" => new Point(-1, 0),
            "R" => new Point(1, 0),
            "U" => new Point(0, -1),
            "D" => new Point(0, 1),
            _ => throw new InvalidOperationException()
        };

        return Enumerable.Repeat(moving, move);
    }

    record struct Point(int X, int Y)
    {
        public static Point operator +(Point a, Point b) => new(a.X + b.X, a.Y + b.Y);
        public bool IsAdjacency(Point a) => Math.Abs(X - a.X) <= 1 && Math.Abs(Y - a.Y) <= 1;
        public Point GetNearest(Point to) => (to.X - X, to.Y - Y) switch
        {
            (< 0, < 0) => new(X - 1, Y - 1),
            (< 0, 0) => new(X - 1, Y),
            (< 0, > 0) => new(X - 1, Y + 1),
            (0, > 0) => new(X, Y + 1),
            (> 0, > 0) => new(X + 1, Y + 1),
            (> 0, 0) => new(X + 1, Y),
            (> 0, < 0) => new(X + 1, Y - 1),
            (0, < 0) => new(X, Y - 1),
            _ => throw new InvalidOperationException()
        };
    }

    static void Draw(IEnumerable<Point> points)
    {
        var xLeft = points.Min(x => x.X);
        var xRight = points.Max(x => x.X);
        var yTop = points.Min(x => x.Y);
        var yBottom = points.Max(x => x.Y);

        var map = new string[xRight - xLeft + 1, yBottom - yTop + 1];
        foreach (var point in points)
        {
            map[-xLeft + point.X, -yTop + point.Y] = "#";
        }
        for (var y = 0; y < map.GetLength(1); y++)
        {
            for (var x = 0; x < map.GetLength(0); x++)
            {
                if (map[x, y] == "#")
                    Console.Write("#");
                else
                    Console.Write(".");
            }
            Console.WriteLine();
        }

    }

    public static string Solve2(string input)
    {
        // 입력에서 헤드의 이동 얻기
        var headHistory = input
            .Split(Environment.NewLine)
            .Select(x => ParseHeadMove(x))
            .SelectMany(x => x)
            .Aggregate(Enumerable.Empty<Point>().Append(new Point(0, 0)), (items, item) => items.Append(items.Last() + item)) // 좀 더 적절한 LINQ 함수 찾아야 함
            .ToArray();

        var tails = new List<Point>(Enumerable.Repeat(new Point(0, 0), 9));
        var tailHistory = new List<Point>(tails);

        foreach (var head in headHistory)
        {
            Point before = head;
            for (var i = 0; i < tails.Count; i++)
            {
                var tail = tails[i];

                if (tail.IsAdjacency(before) is false)
                {
                    tails[i] = tail = tail.GetNearest(before);
                        
                    if (i == tails.Count - 1)
                        tailHistory.Add(tail);
                }

                before = tail;
            }
        }

        //Draw(tailHistory);

        return tailHistory.Distinct().Count().ToString();
    }
}

구현한 Draw()를 통해 심화 문제의 궤적을 확인할 수 있는데요
image

다른 분들의 풀이가 궁금합니다.

1개의 좋아요

Day10

noopaddx로 이루어진 간단한 기계가 있습니다. noop은 한 사이클을 쉬고 addx는 2 사이클을 소비한 후 레지스터 값에 값을 더합니다.

noop
addx 3
addx -5

레지스터 기본 값: 1

  • noop - 한 사이클을 쉽니다.
  • addx 3 - 두 사이클 소비 후 1 + 3 = 레지스터를 4로 만듭니다.
  • addx -5 - 두 사이클 소비 후 4 + -5 = 레지스터를 -1로 만듭니다.

일반: 20, 60, 100, 140, 180, 220 사이클에 레지스터 값을 사이클 * 값으로 계산해서 총 합을 구합니다.
심화: 특정 조건을 통해 화면을 구성하는 시그널이 됩니다. 그 시그널을 이용해 화면을 구성합니다.

public static string Solve1(string input)
{
    var commands = input.Split(Environment.NewLine);
    var machine = new Machine();
    machine.Load(commands);
    var sum = machine.Run()
        .Take(220)
        .Where(x => x.Cycle is 20 or 60 or 100 or 140 or 180 or 220)
        .Select(x => x.Cycle * x.Value)
        .Sum();

    return sum.ToString();
}

public static string Solve2(string input)
{
    var commands = input.Split(Environment.NewLine);
    var machine = new Machine();
    machine.Load(commands);
            
    var sb = new StringBuilder();
    foreach (var (cycle, value) in machine.Run().Take(240))
    {
        var xOffset = (cycle - 1) % 40;

        if (xOffset >= value - 1 && xOffset <= value + 1)
            sb.Append('#');
        else
            sb.Append('.');

        if (xOffset is 39)
            sb.AppendLine();
    }

    return sb.ToString();
}

class Machine
{
    private int _rX = 1;
    private Command[]? _commands;

    public int RegisterX => _rX;

    public void Load(string[] commands)
    {
        _commands = commands.Select(Command.Parse).ToArray();
    }

    private IEnumerable<Command> NextCommand()
    {
        if (_commands is null || _commands.Length is 0)
            yield break;

        var index = 0;
        while (true)
        {
            var command = _commands[index % _commands.Length];
            yield return command;
                    
            index++;
        }
    }

    public IEnumerable<(int Cycle, int Value)> Run()
    {
        var rX = 1;
        var cycle = 0;

        foreach (var command in NextCommand())
        {
            foreach(var _ in Enumerable.Range(1, command.Cycles))
            {
                cycle++;

                yield return (cycle, rX);
            }

            if (command is AddxCommand addxCommand)
                rX += addxCommand.IncValue;
        }
    }
}

abstract record Command()
{
    public abstract int Cycles { get; }

    public static Command Parse(string input)
    {
        var tokens = input.Split(' ', StringSplitOptions.TrimEntries);
        if (tokens[0] is "noop")
            return new NoopCommand();
        else if (tokens[0] is "addx")
            return new AddxCommand(int.Parse(tokens[1]));

        throw new InvalidOperationException();
    }
}
record NoopCommand() : Command
{
    public override int Cycles => 1;
}
record AddxCommand(int IncValue) : Command
{
    public override int Cycles => 2;
}

image

1개의 좋아요

다른 분들의 코드도 분석해봅시다.

엔젤라님의 코드 :

윽. 코드가 간결하군요.

마크히스님 코드:

역시 간결하군요.

어떻게 똑같은 코드가 없네요… ^^;

1개의 좋아요

Day 11

드이어 막힌 문제가 생겼습니다. Day 11의 첫번쨰 문제는 무난히 해결했는데
두번째 심화 문제는 값이 기하급수로 커지므로 BigInteger를 사용했으나 역부족이였습니다.
… --;


풀었습니다.

두번째 심화 문제는 Operation 결과 값을 구하는 것은 아니므로 제대로 Test 될 수 있게 모든 원숭이의 테스트 나누기 값을 곱한 값이 최대 값이 되도록

mulDivition = 23 * 19 * 13 * 17 // 96577
newOperationResult = operationResult % MulDivition

하였습니다. 이를 통해 Operation 결과 값이 무한히 증가하는 것을 막을 수 있습니다.

public static string Solve1(string input)
{
    var monkeys = input.Split(Environment.NewLine)
        .Chunk(7)
        .Select(x => Monkey.Parse(x, 3))
        .ToArray();

    var targetRound = 20;

    foreach (var _ in Enumerable.Range(1, targetRound))
    {
        foreach (var monkey in monkeys)
        {
            monkey.Test((number, value) =>
            {
                var handingMonkey = monkeys.First(x => x.Number == number);
                handingMonkey.Has(value);
            });
        }
    }

    return monkeys
        .OrderByDescending(x => x.Times)
        .Take(2)
        .Aggregate(1, (multi, monkey) => multi * monkey.Times)
        .ToString();
}

public static string Solve2(string input)
{
    var monkeys = input.Split(Environment.NewLine)
        .Chunk(7)
        .Select(x => Monkey.Parse(x, 1))
        .ToArray();

    var mulDivision = monkeys.Aggregate(1UL, (multi, monkey) => multi * (ulong)monkey.TestDivision);

    var targetRound = 10000;

    foreach (var round in Enumerable.Range(1, targetRound))
    {
        //Console.WriteLine(round);
        foreach (var monkey in monkeys)
        {
            monkey.Test((number, value) =>
            {
                var handingMonkey = monkeys.First(x => x.Number == number);
                handingMonkey.Has(value % mulDivision);
            });
        }
    }

    return monkeys
        .OrderByDescending(x => x.Times)
        .Take(2)
        .Aggregate(1L, (multi, monkey) => multi * monkey.Times)
        .ToString();
}

class Monkey
{
    private Func<ulong, ulong> _operation;
    private Queue<ulong> _items;
    private int _ridiculousLevel;
    private int _testDivision;
    private int _divisibleTrueMonkeyNumber;
    private int _divisibleFalseMonkeyNumber;

    public int Number { get; }

    public int Times { get; private set; }

    public int TestDivision => _testDivision;


    private Monkey(int number, ulong[] items, Func<ulong, ulong> operation, int ridiculousLevel, int testDivision, int divisibleTrueMonkeyNumber, int divisibleFalseMonkeyNumber)
    {
        Number = number;
        _items = new Queue<ulong>(items);
        _operation = operation;
        _ridiculousLevel = ridiculousLevel;
        _testDivision = testDivision;
        _divisibleTrueMonkeyNumber = divisibleTrueMonkeyNumber;
        _divisibleFalseMonkeyNumber = divisibleFalseMonkeyNumber;
    }

    public void Test(Action<int, ulong> handingFunc)
    {
        var count = _items.Count;

        while (_items.Count > 0)
        {
            var item = _items.Dequeue();
            var result = _operation(item);
            var testResult = result / (ulong)_ridiculousLevel;
            if (testResult % (ulong)_testDivision == 0)
                handingFunc(_divisibleTrueMonkeyNumber, testResult);
            else
                handingFunc(_divisibleFalseMonkeyNumber, testResult);
        }

        Times += count;
    }

    public void Has(ulong value)
    {
        _items.Enqueue(value);
    }

    public static Monkey Parse(string[] strings, int ridiculousLevel)
    {
        var number = int.Parse(strings[0][7..8]);
            
        var items = strings[1][18..]
            .Split(',', StringSplitOptions.TrimEntries)
            .Select(ulong.Parse)
            .ToArray();
            
        var strOperation = strings[2][19..];
            
        var operation = (ulong x) => x;
        if (strOperation is "old * old")
            operation = x => x * x;
        else if (strOperation.StartsWith("old +") is true)
        {
            var value = ulong.Parse(strOperation[5..]);
            operation = x => x + value;
        }
        else if (strOperation.StartsWith("old *") is true)
        {
            var value = ulong.Parse(strOperation[5..]);
            operation = x => x * value;
        }
        else
            throw new InvalidOperationException();

        var testDivision = int.Parse(strings[3][21..]);
        var divisibleTrueMonkeyNumber = int.Parse(strings[4][29..]);
        var divisibleFalseMonkeyNumber = int.Parse(strings[5][30..]);

        return new Monkey(number, items, operation, ridiculousLevel, testDivision, divisibleTrueMonkeyNumber, divisibleFalseMonkeyNumber);
    }
}
1개의 좋아요

이번 AoC의 여정은 LINQ 습득과 좀 더 효율적인 (그러나 가독성은 유지하는) 코드를 작성하는 여정입니다. 나아가 단위 테스트를 만들면서 원시 문자열로 인해 깔끔하게 특성을 부여할 수 있어서 좋았습니다.

    [TestCase("""
        Sabqponm
        abcryxxl
        accszExk
        acctuvwj
        abdefghi
        """, ExpectedResult = "31")]
    public string Day12_Test1(string input)
    {
        return Day12.Solve1(input);
    }
3개의 좋아요