วงแหวนเว็บ

neizod's speculation

insufficient data for meaningful answer

Code Jam 2018 รอบคัดเลือก

หลังจากพลาดท่าอย่างไม่น่าให้อภัยไปในปี 2014 ปีนี้ก็ถึงเวลาได้คะแนนเต็มซักที 😂

… ซึ่งเอาจริงๆ อาจจะเป็นเพราะ Code Jam เปลี่ยนกฎกติการระบบการแข่งใหม่ เลยแต่งโจทย์ครั้งแรกให้มันง่ายลงก็ได้ ซึ่งด้วยความไม่ชินกับระบบใหม่นี้ เลยทำให้รอบนี้เราเลือกเขียนแต่ Python ที่ถนัดที่สุดเพียงภาษาเดียว


Saving The Universe Again

ข้อง่ายข้อแรก แค่แปลงรหัสคำสั่งของเอเลี่ยนให้กลายเป็นผลบวกพลังโจมตีในรูปนี้

\[\text{damage} = a_0 2^0 + a_1 2^1 + a_2 2^2 + \dots + a_n 2^n\]

โดยที่แต่ละ $a_i$ คือจำนวนการปรากฏของคำสั่ง S ที่มีคำสั่ง C ปรากฏอยู่ก่อนหน้าเป็นจำนวณ $i$ ครั้ง

ต่อมาก็ดูว่าค่า damage มีมากกว่า shield หรือเปล่า? ถ้าใช่ก็ลดค่า $a_x; x = \max_{a_i \neq 0} i$ ลงไปหนึ่ง แล้วเพิ่มค่าใน $a_{x-1}$ ขึ้นมาหนึ่งแทน เสร็จแล้วก็วนคำนวณค่า damage ไปเรื่อยๆ จนกว่า shield จะป้องกันได้นั่นเอง

def make_attack_group(instructions):
    ls = [0]
    for inst in instructions:
        if inst == 'S':
            ls[-1] += 1
        else:
            ls += [0]
    return ls

def min_swap(shield, instructions):
    attack_group = make_attack_group(instructions)
    if sum(attack_group) > shield:
        return 'IMPOSSIBLE'
    time_swap = 0
    while shield < sum(a*2**i for i, a in enumerate(attack_group)):
        if attack_group[-1] == 0:
            attack_group.pop()
        else:
            time_swap += 1
            attack_group[-1] -= 1
            attack_group[-2] += 1
    return time_swap

for case in range(int(input())):
    raw_shield, instructions = input().split()
    shield = int(raw_shield)
    answer = min_swap(shield, instructions)
    print('Case #{}: {}'.format(case+1, answer))

หมายเหตุ: การ hack ลดค่าโจมตีจากคำสั่งที่มีพลังโจมตีมากที่สุดลงมาเรื่อยๆ จะให้ผลลัพธ์เป็นจำนวนครั้งที่ต้อง hack น้อยสุดเสมอ ซึ่งสามารถพิสูจน์ด้วยเทคนิคพื้นฐานที่เรียกว่า greedy stays ahead ครับ


Trouble Sort

ถ้า implement วิธีจัดเรียงตามที่โจทย์บอก จะกินเวลาเป็น $O(n^2)$ ซึ่งไม่น่าส่งคำตอบได้ทัน (หรือถึงแม้ทันก็ไม่เท่) ดังนั้นมาสังเกตสมบัติตลกๆ ของการจัดเรียงแบบสารพันปัญหานี้

                 swap    swap    swap
              v-------v-------v-------v-------
sequence = [ a0, a1, a2, a3, a4, a5, a6, a7, ..., an ]
                  ^-------^-------^-------^---
                     swap    swap    swap

จะเห็นว่า ของในตำแหน่งเลขคู่จะถูกสลับได้กับของในตำแหน่งเลขคู่เท่านั้น และเป็นไปในทำนองเดียวกันกับของที่อยู่ในตำแหน่งเลขคี่

ดังนั้นอัลกอริทึมการจัดเรียงที่โจทย์บอก จึงสามารถถูกเลียนแบบได้ด้วยการจัดเรียงของในตำแหน่งเลขคู่และเลขคี่แยกกันนั่้นเอง

การจัดเรียงสิ่งของด้วยอัลกอริทึมอื่นที่ดีพอ จะกินเวลาแค่ $O(n \log n)$ เท่านั้น ซึ่งก็คือเราสามารถเร่งความเร็วอัลกอริทึมเลียนแบบของโจทย์ได้ด้วยเวลาที่น้อยลง จึงควรจะแก้โจทย์ข้อนี้ได้ทันไม่มีปัญหา

from itertools import count

def trouble_sort(ls):
    ls[0::2] = sorted(ls[0::2])
    ls[1::2] = sorted(ls[1::2])
    return ls

def check_trouble_sort(ls):
    ls = trouble_sort(ls)
    for index, a, b in zip(count(), ls, ls[1:]):
        if a > b:
            return index
    return 'OK'

for case in range(int(input())):
    input()
    ls = [int(n) for n in input().split()]
    answer = check_trouble_sort(ls)
    print('Case #{}: {}'.format(case+1, answer))

Go, Gopher!

เนื่องจากการสั่งขุดพื้น 1 ช่องมีความน่าจะเป็นที่จะขุดช่องนั้นได้เพียง 1 ใน 9 จึงไม่ควรไปหวังว่าจะขุดเป็นพื้นที่สี่เหลี่ยมสวยๆ ด้วยการบอกตำแหน่งเป๊ะๆ ได้

สิ่งที่ทำได้คือเปลี่ยนไปมองว่าต้องการสั่งขุดสี่เหลี่ยมขนาด 3x3 ช่องไปเลย (ผ่านการขุดหลายครั้งโดยการบอกตำแหน่งเดิมซ้ำๆ) เรียบร้อยแล้วจึงจับเอาสี่เหลี่ยมจัตุรัสที่เพิ่งสร้างได้นี้ไปสร้างเป็นสี่เหลี่ยมใหญ่ๆ ตามที่โจทย์ต้องการต่อไป

เราสามารถใช้ coupon collector’s problem มาประมาณได้ว่าการขุดรูปสี่เหลี่ยมจัตุรัสนี้ จะต้องขุดประมาณกี่ครั้งถึงจะสำเร็จ ซึ่งก็คือ

\[\begin{align*} \mathbf{E}(T) &= \frac{n}{1} + \frac{n}{2} + \frac{n}{3} + \dots + \frac{n}{n} \\ &= \frac{9}{1} + \frac{9}{2} + \frac{9}{3} + \dots + \frac{9}{9} \\ &= 25.4607 \end{align*}\]

โจทย์ต้องการให้ขุดพื้นที่มากที่สุด 200 ช่อง ซึ่งก็คือต้องขุดสี่เหลี่ยมจัตุรัสเล็กๆ นี้ทั้งหมด 23 รูป แต่ละรูปประมาณว่าขุด 26 ครั้งจึงเสร็จ คูณกันแล้วน่าจะขุดประมาณ 598 ครั้ง ซึ่งน้อยกว่าขีดจำกัดที่ให้ขุดได้ 1000 ครั้งไปเกือบครึ่ง ดังนั้นน่าจะไม่ซวยขุดดินที่เดิมซ้ำๆ จนเหนื่อยแต่ไม่ได้สนามอย่างที่พอใจเนาะ 😉

def interact(size):
    x = 42
    y = 23
    holes_dug = set()
    while True:
        print(x, y)
        rx, ry = [int(n) for n in input().split()]
        if (rx, ry) == (-1, -1):
            exit(42)
        elif (rx, ry) == (0, 0):
            break
        holes_dug |= {(rx, ry)}
        if len(holes_dug) == 9:
            holes_dug.clear()
            y += 3

for case in range(int(input())):
    size = int(input())
    interact(size)

หมายเหตุ: Python ไม่ต้องพึ่ง sys.stdout.flush ก็พ่นผลลัพธ์ให้เลยทุกครั้งที่มีการขึ้นบรรดทัดใหม่


Cubic UFO

ข้อนี้เริ่มขี้เกียจอธิบายละ เอาที่ทดบนไวท์บอร์ดไปแกะเองละกันนะ 555

สูตรคำนวณพื้นที่เงา UFO เมื่อ UFO หมุนตามแกนต่างๆ

import math

def rotate(point, x_angle, z_angle):
    point = ( point[0],
              math.cos(x_angle) * point[1] - math.sin(x_angle) * point[2],
              math.sin(x_angle) * point[1] + math.cos(x_angle) * point[2], )
    point = ( math.cos(z_angle) * point[0] - math.sin(z_angle) * point[1],
              math.sin(z_angle) * point[0] + math.cos(z_angle) * point[1],
              point[2], )
    return point

def get_points_from_rotation(x_angle, z_angle):
    points = [(0.5, 0, 0), (0, 0.5, 0), (0, 0, 0.5)]
    return [rotate(point, x_angle, z_angle) for point in points]

def hexagon_shadow(angle):
    return 2**0.5 * math.cos(angle) + math.sin(angle)

def rotate_two_axis(area):
    lower = 0
    upper = math.atan(1/2**0.5)
    while True:
        angle = (lower+upper) / 2
        guess_area = hexagon_shadow(angle)
        if math.isclose(guess_area, area):
            break
        elif guess_area < area:
            lower = angle
        elif guess_area > area:
            upper = angle
    return math.radians(45), angle

def rotate_one_axis(area):
    angle = math.radians(45) - math.acos(area/2**0.5)
    return angle, 0

def compatible_points(area):
    if area <= 2**0.5:
        x_angle, z_angle = rotate_one_axis(area)
    else:
        x_angle, z_angle = rotate_two_axis(area)
    return get_points_from_rotation(x_angle, z_angle)

for case in range(int(input())):
    area = float(input())
    answers = compatible_points(area)
    print('Case #{}:'.format(case+1))
    for point in answers:
        print(' '.join(str(num) for num in point))

หมายเหตุ: จุดที่ขาดหายไปจากการทดบนกระดานคือ rotation matrix (ซึ่งน่าจะเป็นเรื่องพื้นฐานได้แล้วนะ) ถ้าใครยังไม่รู้จักคุ้นเคย ไปดูรูปตัวอย่างพร้อมสมการจากลิงก์ wiki น่าต้นจะเข้าใจได้ในเวลาไม่นานครับ


ด้วยคะแนนเต็มครั้งนี้ ทำให้ rank ต่ำกว่าพันแล้ว! มีโอกาสได้เสื้อมากกว่าปีไหนๆ เลย 🙌

ไว้ลุ้นกันใหม่อีกครั้งในรอบหนึ่งครับ

ป.ล. โดน penalty ข้อแรกไป 5 ครั้งเพราะ server เอ๋อ เราเลยนึกว่ากดส่งคำตอบแล้วยังไม่ไป เลยกดส่งโค้ดเดิมมันซ้ำๆ อยู่นั่นแหละ 🙄

ป.ล.ล. ระบบใหม่ทำ friend list หาย อดส่อง (ง่ายๆ) เลยว่าเพื่อนๆ ได้คะแนนเท่าไหร่กันมั่ง แถมยังโหลดโค้ดคนอื่นมาดูไม่ได้แล้ว 😭

neizod

author