Advertisement
eltea

Advent of Code 2020 Day 16 Parts 1 and 2

Dec 16th, 2020
156
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C# 10.01 KB | None | 0 0
  1. //Advent of Code 2020 Day 16 Parts 1 and 2 solution by Mike LeSauvage
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using System.Text.RegularExpressions;
  5.  
  6.  
  7. public class TicketAnalyzer : MonoBehaviour
  8. {
  9.     [SerializeField] TextAsset ticketRulesTextfile = null;   //Hooked up in the input text in the Unity editor.
  10.  
  11.     string[] ticketRulesAndTicketStrings;  //Holds all the individual lines from the text file.
  12.     int ticketRuleStringIndex = 0;         //Indexes into the ticketRulesAndStrings array as the pointer of the next line to analyze.
  13.  
  14.     string fieldRuleRegexString = @"^([\w ]+): (\d+)-(\d+) or (\d+)-(\d+)$"; //Grabs field name, range1start, range1end, range2start, range2end
  15.     string ticketRegexString = @"^(?:\d+,){19}\d+$";                         //Checks for a string of 20 comma-delimited numbers.
  16.     Regex ticketRgx;
  17.  
  18.  
  19.     // TicketRule has the name of a ticket field and the two ranges that are valid for that field.
  20.     // It also contains data and logic for matching the rule to a field on the ticket.
  21.     class TicketRule
  22.     {
  23.         public string name;
  24.         public int range1Start;
  25.         public int range1End;
  26.         public int range2Start;
  27.         public int range2End;
  28.         public List<int> possibleMatchingFields; //All possible ticket fields that match the allowable ranges.
  29.         public int matchingField;                //The specific and only actual matching field.
  30.  
  31.         // Pretty print for debugging.
  32.         public override string ToString()
  33.         {
  34.             return $"Field {name} valid ranges: {range1Start}-{range1End} | {range2Start}-{range2End}";
  35.         }
  36.  
  37.         // Method to check if a number falls within the allowable ranges for this rule.
  38.         public bool IsNumberInAnyRuleRange(int num)
  39.         {
  40.             if( (num >= range1Start && num <=range1End) || (num >= range2Start && num <= range2End) )
  41.             {
  42.                 return true;
  43.             }
  44.             return false;
  45.         }
  46.     }
  47.  
  48.     List<TicketRule> ticketRules = new List<TicketRule>(); //Holds all ticket rules.
  49.     const int NUM_TICKET_FIELDS = 20;
  50.     int[] myTicket = new int[NUM_TICKET_FIELDS];     //Holds "my" ticket from the puzzle input.
  51.     List<int[]> tickets = new List<int[]>();         //Holds all other provided tickets.
  52.  
  53.  
  54.     // Start is a method in Unity; would be Main() in other environments etc
  55.     void Start()
  56.     {
  57.         ticketRgx = new Regex(ticketRegexString); //Regular expression to check if a string is 20 comma-delimited numbers.
  58.         string[] stringSeparator = { "\r\n" };    //Find two new lines for breaks. Windows encoding has both carriage return and line feed.
  59.         ticketRulesAndTicketStrings = ticketRulesTextfile.text.Split(stringSeparator, System.StringSplitOptions.RemoveEmptyEntries);
  60.  
  61.         //Read in the rules.
  62.         while(ReadRule(ticketRulesAndTicketStrings[ticketRuleStringIndex]))
  63.         {
  64.             ticketRuleStringIndex++;
  65.         }
  66.  
  67.         //Read in my ticket.
  68.         AdvanceTicketIndexToNextTicket();
  69.         myTicket = ReadTicket(ticketRulesAndTicketStrings[ticketRuleStringIndex]);
  70.  
  71.         //Read in all the "nearby" tickets.
  72.         ticketRuleStringIndex++;
  73.         AdvanceTicketIndexToNextTicket();
  74.         while(ticketRuleStringIndex < ticketRulesAndTicketStrings.Length)
  75.         {
  76.             tickets.Add(ReadTicket(ticketRulesAndTicketStrings[ticketRuleStringIndex]));
  77.             ticketRuleStringIndex++;
  78.         }
  79.  
  80.         //Solve Part One
  81.         //--------------
  82.         int errors = RemoveInvalidTickets();
  83.         Debug.Log($"Part 1 - Error Scanning Rate: {errors}. {tickets.Count} tickets remaining.");
  84.  
  85.         //Add my ticket to those being used to match rules to fields. (Part 1 told us to ignore it!)
  86.         tickets.Add(myTicket);
  87.  
  88.         //Solve Part Two
  89.         //--------------
  90.         // Start by finding which fields are possible matches for which rules.
  91.         // A test of this found that the puzzle has been built such that:
  92.         //    Rule A - 20 matches
  93.         //    Rule B - 19 matches
  94.         //    Rule C - 18 matches
  95.         // etc... so that once the possible matches are found, the specific matches can be identified by
  96.         // taking the "matches 1 only" match, assigning it, and removing it from all the other possible
  97.         // matches, then repeating for the new "matches 1 only" match.
  98.  
  99.         //For every rule rule
  100.         //  Iterate through each field, one at a time
  101.         //    Across every ticket.
  102.         //      If all fields pass the rule, then it's a match (that will have to get winnowed down later).
  103.         foreach(TicketRule rule in ticketRules)
  104.         {
  105.             for(int i=0; i<NUM_TICKET_FIELDS; i++)
  106.             {
  107.                 bool passedAllTickets = true;
  108.  
  109.                 foreach(int[] t in tickets)
  110.                 {
  111.                     if(!rule.IsNumberInAnyRuleRange(t[i]))
  112.                     {
  113.                         passedAllTickets = false;
  114.                         break;
  115.                     }
  116.                 }
  117.  
  118.                 if(passedAllTickets)
  119.                 {
  120.                     rule.possibleMatchingFields.Add(i);
  121.                 }
  122.             }
  123.         }
  124.  
  125.         //Match rules to fields by working through all the rules.
  126.         //  In two loops, swapping the found rule to the front so each rule with one match is only encountered once.
  127.         //    Find the rule that matches only one field.
  128.         //    Mark the final matching field on the rule for convenenience (it could have stayed just in the list).
  129.         //    Remove it from all the other rules
  130.         //    Swap it to the front so that it isn't checked again.
  131.         //
  132.         //  Repeat until they are all matched.
  133.         for (int i=0; i<ticketRules.Count-1; i++)
  134.         {
  135.             TicketRule curRule = ticketRules[i];
  136.            
  137.             for (int j = i; j < ticketRules.Count; j++)
  138.             {
  139.                 if (ticketRules[j].possibleMatchingFields.Count == 1)
  140.                 {
  141.                     ticketRules[j].matchingField = ticketRules[j].possibleMatchingFields[0];
  142.                     RemovePossibleMatchFromAllOtherRules(ticketRules[j]);
  143.                     ticketRules[i] = ticketRules[j];
  144.                     ticketRules[j] = curRule;
  145.                     break;
  146.                 }
  147.             }
  148.         }
  149.  
  150.         // At this point every rule has a matching field.
  151.         // Loop through all the rules
  152.         //   For each rule matched to a field with the word "departure"
  153.         //     Get the value from MY ticket
  154.         //     Multiply them together.
  155.         long partTwoSolution = 1;
  156.         foreach(TicketRule tr in ticketRules)
  157.         {
  158.             if(tr.name.Contains("departure"))
  159.             {
  160.                 partTwoSolution *= myTicket[tr.matchingField];
  161.             }
  162.         }
  163.  
  164.         Debug.Log($"Part Two Solution: {partTwoSolution}");
  165.  
  166.     }
  167.  
  168.     // Looks at a rule's matching field and removes that from the list held by all other rules.
  169.     void RemovePossibleMatchFromAllOtherRules(TicketRule targetRule)
  170.     {
  171.         int count = 0;
  172.         foreach(TicketRule tr in ticketRules)
  173.         {
  174.             if(tr != targetRule)
  175.             {
  176.                 if(tr.possibleMatchingFields.Remove(targetRule.matchingField))
  177.                 {
  178.                     count++;
  179.                 }
  180.             }
  181.         }
  182.     }
  183.  
  184.     //Check all ticket values. Iterate backwards so tickets with errors can be removed.
  185.     int RemoveInvalidTickets()
  186.     {
  187.         int errorScanningRate = 0;
  188.        
  189.         for (int i = tickets.Count - 1; i >= 0; i--)
  190.         {
  191.             int[] ticket = tickets[i];
  192.             foreach (int num in ticket)
  193.             {
  194.                 bool ruleFound = false;
  195.                 foreach (TicketRule tr in ticketRules)
  196.                 {
  197.                     if (tr.IsNumberInAnyRuleRange(num))
  198.                     {
  199.                         ruleFound = true;
  200.                     }
  201.                 }
  202.                 if (!ruleFound)
  203.                 {
  204.                     errorScanningRate += num;
  205.                     tickets.RemoveAt(i);
  206.                 }
  207.             }
  208.         }
  209.  
  210.         return errorScanningRate;
  211.     }
  212.  
  213.  
  214.     // Used for advancing through the text file to find the next ticket.
  215.     // Moves the ticketRuleStringIndex along until it is pointing at a string of
  216.     // twenty comma-separated numbers.
  217.     void AdvanceTicketIndexToNextTicket()
  218.     {
  219.         while (!ticketRgx.IsMatch(ticketRulesAndTicketStrings[ticketRuleStringIndex]))
  220.         {
  221.             ticketRuleStringIndex++;
  222.         }
  223.     }
  224.  
  225.  
  226.     // Takes in a single rule in the format of: field name: r1s-r1e or r2s-r2e
  227.     //                                      eg: departure location: 29-766 or 786-950
  228.     // Breaks it apart and stores it into a new TicketRule, which is added to the
  229.     // ticketRules list.
  230.     bool ReadRule(string ticketString)
  231.     {
  232.         MatchCollection allMatches = Regex.Matches(ticketString, fieldRuleRegexString);
  233.         if (allMatches.Count == 0)
  234.         {
  235.             return false;
  236.         }
  237.  
  238.         GroupCollection groups = allMatches[0].Groups;
  239.  
  240.         TicketRule rule = new TicketRule();
  241.         rule.name = groups[1].Value;
  242.         rule.range1Start = int.Parse(groups[2].Value);
  243.         rule.range1End = int.Parse(groups[3].Value);
  244.         rule.range2Start = int.Parse(groups[4].Value);
  245.         rule.range2End = int.Parse(groups[5].Value);
  246.         rule.possibleMatchingFields = new List<int>();
  247.         rule.matchingField = -1;
  248.  
  249.         ticketRules.Add(rule);
  250.  
  251.         return true;
  252.     }
  253.  
  254.  
  255.     // Separates a comma-delimited list of numbers and returns them as an array of integers.
  256.     int[] ReadTicket(string ticket)
  257.     {
  258.         int[] ticketAsNumbers = new int[NUM_TICKET_FIELDS];
  259.  
  260.         string[] allTicketNumbers = ticket.Split(',');
  261.        
  262.         for(int i=0; i<NUM_TICKET_FIELDS; i++)
  263.         {
  264.             ticketAsNumbers[i] = int.Parse(allTicketNumbers[i]);
  265.         }
  266.  
  267.         return ticketAsNumbers;
  268.     }
  269. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement