Showing posts with label One-Liner. Show all posts
Showing posts with label One-Liner. Show all posts

Nov 10, 2013

Division ใน Relational Algebra

การหาร (DIVISION) ใน relational algebra อธิบายง่ายๆ คือการเอาตารางมา join กันแล้วนับว่าแต่ละ pk ของตารางตัวตั้ง มี row เท่ากับตารางตัวหารหรือไม่ ถ้าครบก็ตัด column ที่ปรากฏในตารางตัวหารทิ้งแล้วยุบรวม row ซ้ำด้วย pk เช่นเรามีข้อมูล
table: students
------+------+-------
 name | subj | grade 
------+------+-------
 bar  | code | A
 foo  | code | C+
 foo  | trek | A
 qux  | code | B+
 qux  | math | A
 qux  | trek | A
------+------+-------

table: colleges
-----------+------
  campus   | subj 
-----------+------
 leetademy | code
 leetademy | trek
 hack-cool | code
 hack-cool | math
-----------+------
คำสั่ง SQL ของการหารก็จะมีหน้าตาประมาณ
SELECT *
FROM   students
DIVISION (
    SELECT subj
    FROM   colleges
    WHERE  campus = 'leetademy'
) AS leetademy_courses
ซึ่งให้ผลลัพท์ออกมาคือ
------
 name 
------
 foo
 qux
------



ปัญหาของการหารคือ DBA แทบทั้งหมดไม่มีคำสั่งนี้ให้ใช้ (ต่างจากการลบ (MINUS) ที่ยังมีบ้าง แม้บางเจ้าจะเปลี่ยนชื่อเป็น EXCEPT ก็ตาม) เพราะมันเป็นท่าที่เปลืองทรัพยากรมาก แถมไม่ได้ใช้กันบ่อยๆ เสียด้วย ...ซึ่งจะว่าไปแล้วมันก็ดูไม่ใช่ปัญหาเลยนี่หน่า

อย่างไรก็ตามปัญหาจริงๆ คือมันเป็นท่าที่อาจารย์ database ส่วนใหญ่ชอบเอาไปออกสอบ ทำให้เราต้องหาท่าอ้อมๆ ที่ให้ผลลัพท์แบบเดียวกันมาใช้แทน

การจะเข้าใจมันได้ ถ้าคิดแบบ programming ก็แค่ดูว่า set ที่เป็นตัวตั้งหารนั้นเป็น superset ของ set ที่ถูกหารหรือเปล่า
students_set = defaultdict(set)
for name, subj, grade in students:
    students_set[name].add(subj)

colleges_set = defaultdict(set)
for campus, subj in colleges:
    colleges_set[campus].add(subj)

output = set()
for name, subj_set in students_set.items():
    if subj_set >= colleges_set['leetademy']:
        output.add(name)
print(output)
แต่การทำงานบน database เรามักเขียนพวกนี้ทั้งหมดให้เป็น one-liner เพื่อที่จะรับประกันว่าระหว่างการ query จะไม่เกิด race condition กับ query อื่นๆ สิ่งที่ต้องทำก่อนอื่นใดคือพยายามกำจัดการสร้าง set ของข้อมูลล่วงหน้าซะ

ในที่นี้ ขั้นแรกเราจะกำจัดการสร้าง colleges_set โดยวน loop ใน colleges ทุกตัวดูว่าแต่ละ subj มีใน students_set หรือเปล่า
def learnt_all(name):
    for campus, subj in colleges:
        if campus == 'leetademy':
            if subj not in students_set[name]:
                return False
    return True

output = set()
for name, _, _ in students:
    if learnt_all(name):
        output.add(name)
print(output)
ขั้นต่อมาก็กำจัด students_set โดย loop เข้า students ดูว่านักเรียนที่ชื่อ name ได้เรียนวิชา subj แล้วหรือยัง
def learnt(name, subj):
    for student_name, student_subj, _ in students:
        if student_name == name and student_subj == subj:
            return False
    return True

def learnt_all(name):
    for campus, subj in colleges:
        if campus == 'leetademy':
            if learnt(name, subj):
                return False
    return True
    
output = set()
for name, _, _ in students:
    if learnt_all(name):
        output.add(name)
print(output)
เนื่องจากในฟังก์ชัน learnt กับ learnt_all จะคืนค่า False ทันทีที่เข้าเงื่อนไขครบ แต่จะคืนค่า True หลังจากจบ loop เท่านั้น เราสามารถเขียนตรงนี้ใหม่ด้วย generator แล้ว wrap ด้านนอกด้วย not any(...) เช่นนี้
def inverse_learnt(name, subj):
    for student_name, student_subj, _ in students:
        if student_name == name and student_subj == subj:
            yield True

def inverse_learnt_all(name):
    for campus, subj in colleges:
        if campus == 'leetademy':
            if not any(inverse_learnt(name, subj)):
                yield True

output = set()
for name, _, _ in students:
    if not any(inverse_learnt_all(name)):
        output.add(name)
print(output)
พอทุกอย่างเป็น single-statement และ generator คราวนี้ก็ง่ายที่จะทำ one-liner แล้ว ... จับมาเขียนสวยๆ ได้แบบนี้
print({
    name
    for name, _, _ in students
    if not any(
        True
        for campus, subj in colleges
        if campus == 'leetademy'
        and not any(
            True
            for student_name, student_subj, _ in students
            if student_name == name
            and student_subj == subj
        )
    )
})
เห็น code นี้แล้วคงเดาได้ไม่ยากว่า พอเอาเขียนเป็น SQL คงหนีไม่พ้น
SELECT DISTINCT name
FROM   students AS s
WHERE  NOT EXISTS (
    SELECT *
    FROM   colleges AS c
    WHERE  campus = 'leetademy'
    AND    NOT EXISTS (
        SELECT *
        FROM   students AS ss
        WHERE  ss.name = s.name
        AND    ss.subj = c.subj
    )
)

อ้างอิง


Sep 3, 2012

แก้โจทย์ป้ายสมัครงาน Google แบบบรรทัดเดียว

นอนไม่หลับ บวกกับช่วงนี้แก้โจทย์เล่นเยอะไปหน่อย เหมือนสมองไม่อยากหยุดแก้โจทย์ 555

แล้วก็นึกได้ว่า ไอ้โจทย์ป้ายสมัครงานของ Google อันนี้ ยังไม่เคยแก้เล่นเองเลยนี่หว่า


แต่ถ้าจะให้แก้ตามปรกติธรรมดา ก็ดูจะไม่ท้าทายซักเท่าไหร่ไปแล้ว (เพราะเล่นไปอ่านเฉลยมาเรียบร้อย) แบบนี้เลยต้องเพิ่ม standard ให้กับตัวเองด้วยการเขียนเป็น one-liner โดยไม่พึ่งพา stdlib ซะเลย

ตัวโจทย์คงไม่ต้องแปลแล้ว? (ถ้าตีโจทย์ไม่ออกจริงๆ แนะนำบล็อก @khajochi เลย) เอาเป็นว่าเริ่มกันเลยดีกว่านะ



โดย algorithm ที่จะใช้คำนวณหาค่า e คือ
e = 1 + 1/1! + 1/2! + 1/3! + 1/4! + ...
ต้องใช้ algorithm นี้เพราะว่ามันให้ค่าทศนิยมได้แม่นยำรวดเร็วพอสมควร และยัง implement ตามไม่ยากเท่าไหร่ ในที่นี้ใช้ถึงแค่พจน์ 1/200! ก็ให้ค่าออกมาแม่นยำเกือบ 400 ตำแหน่งแล้ว

มาเริ่มที่ส่วนที่เล็กที่สุด ฟังก์ชั่น factorial ที่ดูเผินๆ ก็ไม่มีอะไรประหลาด แต่ถ้าจะเขียนแบบ one-liner ก็เลี่ยงไม่ได้ที่จะต้องเขียนเป็น recursion และต้องไม่จองชื่อ function ไว้ก่อนด้วย ซึ่งก็ทำได้โดยใช้ combinator แบบนี้
factorial = lambda n: (lambda f, x: f(f, x))(lambda r, y: y*r(r, y-1) if y > 1 else 1, n)
(รายละเอียดอ่านได้จากตอนก่อนๆ)

ต่อมาคือการจับแต่ละพจน์มาบวกกัน ซึ่งจะ implement ตรงๆ แบบนี้ไม่ได้
sum(1/factorial(i) for i in range(200))
เพราะ float มันเก็บ precision ของทศนิยมได้เล็กนิดเดียว ดังนั้นเราต้องดัดแปลงสมการต้นแบบให้กลายเป็น
e = (1 + 1/1! + 1/2! + 1/3! + 1/4! + ...)(1!2!3!4!.../1!2!3!4!...)
เทคนิคหนึ่งที่ใช้ได้เสมอๆ เมื่อต้องการทศนิยม n ตำแหน่ง คือคูณด้วย 10 ยกกำลัง n เข้าไปนั่นเอง ดังนั้น
e*10**n = 10**n * (1 + 1/1! + 1/2! + 1/3! + 1/4! + ...)(1!2!3!4!.../1!2!3!4!...)
        = 10**n * (1 + 2!3!4!... + 1!3!4!... + 1!2!4!... + 1!2!3!... + ...)/1!2!3!4!...
จะเห็นว่าถ้าเรียงลำดับ operator ดีๆ เราจะไม่สูญเสีย precision เลย

ได้แนวคิดแล้วก็เริ่ม implement กัน เริ่มโดยหา factorial ของพจน์ต่างๆ ตามปรกติ
fact_each = [factorial(i) for i in range(200)]
ทีนี้เพื่อป้องกันการคำนวณซ้ำหลายรอบ ก็เอา lambda มาห่อหุ้มมันซะ ซึ่งสิ่งที่เราต้องการต่อจากนี้คือผลคูณรวมของค่าทั้งหมด (1!2!3!4!...) โดยเรายังคงต้องใช้ list เดิมนี้อยู่ ดังนั้น
prod_and_fact_each = (lambda l: [reduce(lambda x, y: x*y, l), l])(fact_each)
ถึงตอนนี้ก็ได้เวลาเอาผลคูณรวมของ factorial ทุกตัว มาหารแต่ละตัวใน list นั้นแล้ว (เรายังต้องใช้ผลคูณรวมอีกรอบ ดังนั้นส่งต่อมันด้วย)
prod_and_div_each = (lambda a, l: [a, [a / i for i in l]])(*prod_and_fact_each)
สุดท้ายก็จับทุกตัวมาบวกกัน คูณด้วย 10 ยกกำลัง n แล้วค่อยหารด้วยค่ารวมนั้น
e = str((lambda a, l: 10**400 * sum(l) / a)(*prod_and_div_each))
เท่านี้ก็ได้เลข e ที่มีทศนิยมตามต้องการแล้วนะ <3



สำหรับส่วนที่สองจะมาดูที่จำนวนเฉพาะ เนื่องจากเลข 10 หลักค่าที่ใหญ่ที่สุดคือ n = 9999999999 การที่จะบอกว่ามันเป็นจำนวนเฉพาะหรือไม่ ก็ทดสอบด้วยการเอาไปหารจำนวนเฉพาะทุกตัวที่น้อยกว่ามันไปจนถึง sqrt(n) ซึ่งหมายความว่าเราตรวจถึงแค่จำนวนเฉพาะที่น้อยกว่า sqrt(9999999999) = 99999 ก็พอแล้ว

เริ่มด้วยฟังก์ชั่นเพิ่มจำนวนเฉพาะเข้าไป
add_prime = lambda p, t: p + [t] if all(t % q for q in p) else p
ฟังก์ชั่นนี้จะรับตัวแปร 2 ตัวคือ list ของจำนวนเฉพาะก่อนหน้า และเลขที่จะตรวจว่าเป็นจำนวนเฉพาะหรือเปล่า ส่วนค่าที่คืนมานั้น ถ้าเป็นจำนวนเฉพาะจะคืน list ที่เพิ่มจำนวนที่ตรวจเข้าไปด้วย ดังนั้นสิ่งที่เราต้องทำคือใส่ list ว่างๆ เข้าไปก่อน แล้วส่งเลขที่น้อยที่สุดคือ 2 เข้าไปตรวจ หลังจากได้ list ที่มีเลข 2 ก็ส่งเลข 3,4,5,6,...,99999 เข้าไปตรวจทีละตัว โดย list ที่เป็นผลลัพท์แต่ละครั้งที่ได้มาก็จะเก็บไว้ใช้ต่อไปเรื่อยๆ นี่ก็คือแนวคิดของ reduce นั่นเอง ซึ่งก็จะเขียนได้ว่า
p = reduce(add_prime, [[], 2] + range(3, 100000, 2))
ถึงตอนนี้ก็มีจำนวนเฉพาะทั้งหมดที่ต้องการสำหรับงานนี้แล้ว :3



สุดท้าย ได้เวลาจับทุกอย่างมารวมกัน เริ่มจากการพยายาม loop ไปยังเลข 10 ตัวในทศนิยมของ e ก่อน ซึ่งก็คือ
[e[i:10+i] for e in [e] for i in range(len(e)-9)]
แต่เนื่องจากเราต้องการ filter ตัวที่ไม่ใช่จำนวนเฉพาะออกไป ซึ่งคราวนี้เราจะเปลี่ยนมาทดสอบด้วยฟังก์ชั่น
test_prime = lambda p, e: all(int(e) % q for q in p)
ดังนั้นก็จะได้
[e[i:10+i] for e, p in [[e, p]] for i in range(len(e)-9) if test_prime(p, e[i:10+i])]
แน่นอนว่างานนี้เราต้องการแค่จำนวนเฉพาะตัวแรกที่เจอ ดังนั้นสามารถเปลี่ยนไปเขียน
next(e[i:10+i] for e, p in [[e, p]] for i in range(len(e)-9) if test_prime(p, e[i:10+i]))
ก็จะได้คำตอบแล้วหละ \(^0^)/



สำหรับ code ฉบับเต็มๆ ที่เป็น one-liner (แต่ขึ้นบรรทัดใหม่เพื่อให้อ่านสะดวก) คือ
next(e[i:10+i] for e, p in [[
        str((lambda a, l: 10**400 * sum(l) / a)(*
            (lambda a, l: [a, [a / i for i in l]])(*
                (lambda l: [reduce(lambda x, y: x*y, l), l])(
                    [(lambda f, x: f(f, x))(
                           lambda r, y: y * r(r, y-1) if y > 1 else 1, i)
                        for i in range(200)])))),
        reduce(
            lambda ps, t: ps + [t] if all(t % p for p in ps) else ps,
            [[], 2] + range(3, 100000, 2))]]
    for i in range(len(e)-9)
        if all(int(e[i:10+i]) % q for q in p))
happy coding นะครับ ^^

Apr 27, 2012

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


สืบเนื่องจาก @Blltz กับ @ngfar มาเที่ยวบ้าน เลยนั่งปั่น Daiblo II กันตั้งแต่ตีสองตีสามจนถึงเจ็ดโมงเช้า กว่าจะเริ่มทำโจทย์ (ซึ่งควรจะเริ่มทำตั้งแต่หกโมงเช้าเลย)

ข้อแรก ง่ายๆ แค่ substitution cipher ธรรมดา ก็โยน input เข้าไป map กันแล้วสร้างเป็น dict ออกมา ทีนี้เวลามันเหลือ เลยเล่นกับ code ซักหน่อย ให้คนตามอ่านปวดหัวเล่น (ของจริงมันอยู่ในบรรทัดเดียว แต่อันนี้แยกบรรทัดให้อ่านง่ายๆ ละนะ) :P
print('\n'.join(
    'Case #{}: {}'.format(
        t+1,
        ''.join(d[c]
            for d in [dict(zip('abcdefghijklmnopqrstuvwxyz ',
                               'yhesocvxduiglbkrztnwjpfmaq '))]
            for c in input()))
    for t in range(int(input()))))
แล้วก็เพิ่งมานึกได้ว่า ไม่ต้องสร้างเป็น dict ก็ได้นี่หว่า...



ข้อถัดมา อันนี้ทดในกระดาษ กระจายรูปแบบออกมาแล้วจะพบว่า ถ้าคะแนน mod 3 แล้วได้ 1 ไม่มีทางที่จะเป็นเซอร์ไพรส์ เลยดูแค่ที่มันไม่ใช่พอ แล้วก็เพิ่มข้อยกเว้นคะแนนแค่ 0 กับ 1 ด้วย
from math import ceil

for t in range(int(input())):
    gg, surprise, expected, *point = (int(n) for n in input().split())
    point.sort(reverse=True)

    count = 0
    for p in point:
        if ceil(p/3) >= expected:
            count += 1
        elif surprise > 0 and ceil(p/3)+1 >= expected:
            if p%3 == 1 or p in (0, 1):
                continue
            count += 1
            surprise -= 1

    print('Case #{}: {}'.format(t+1, count))
ถ้าดูตาม code แล้ว จะเห็นว่าเริ่มมาสั่ง sort ไว้เลย (จริงๆ ไม่ต้องก็ได้) คือตอนนั้นเตรียม optimize ไว้แล้ว แต่เท่าที่จับเวลาดูไม่น่าเป็นห่วง เลยไม่ optimize ดีกว่า เดี๋ยวเจอ bug ซ่อนเร้น



ข้อสาม สมองเริ่มตายเพราะไม่ได้นอนทั้งคืน + เห็นคำว่า recycle เลยเขียนแบบ recursive (ที่ไม่จำเป็นต้อง recursive) ซะเลย 555+
def recycle(m, r):
    global pair, count
    if r > 0:
        m = m%digit * 10 + int(m/digit)
        if lower <= n < m <= upper and m not in pair.setdefault(n, []):
            pair[n].append(m)
            count += 1
        recycle(m, r-1)

for t in range(int(input())):
    lower, upper = [int(n) for n in input().split()]
    round = len(str(lower)) - 1
    digit = 10 ** round

    pair = {}
    count = 0
    for n in range(lower, upper+1):
        recycle(n, round)

    print('Case #{}: {}'.format(t+1, count))
อันนี้ optimize นิดหน่อย ตอนแรกทำการกลับเลขโดยแปลงเป็น str -> สลับที่ str -> แปลงกลับเป็นตัวเลข ปรากฎว่ากินเวลาไปเกินกว่ารับได้ เลยเปลี่ยนวิธีเขียนเป็น math ถ้า test แค่ส่วนกลับเลขก็เร็วกว่าเป็นสิบเท่าเหมือนกัน แต่ไม่ได้ลอง test ทั้ง script ดูว่าเร็วจนน่าพอใจหรือเปล่า

อนึ่ง พอเอาไปรันใน pypy แล้วไม่มีปัญหาเรื่องความเร็วเลยแฮะ เสียเวลา optimize ทำไมเนี่ย

(ข้อนี้แอบโดน @lewcpe ว่าตรงที่ทำไมใช้ global ไม่ return เอาตามธรรมดาวิสัยฟังก์ชั่นทั่วไปด้วย)


พอทำ 3 ข้อแรกเสร็จก็ออกไปเล่นสงกรานต์ กลับมาตอนดึกๆ นั่งมึนหัวอยู่พักนึงแล้วก็คิดวิธีแก้ปัญหาข้อ 4 ได้ (แบบเดียวกับเฉลยบนเว็บ คือสร้างภาพแบบ reflex แล้วใช้วงกลมร้อมรั้วนับเอา) แต่ตอนนั้นคิดวิธี implement ดีๆ ไม่ออกแล้ว แถมเท่าที่ลองเขียนเล่นๆ ก็คิดว่าน่าจะต้องเจองานหนักกว่า 200 บรรทัดแน่ๆ (เวลาไม่น่าพอ) และยังเลือกไม่ถูกอีกว่าจะเขียนแบบ OOP หรือ functional ดี เลยตัดสินใจนอนดีกว่า

แล้วเจอกันรอบ 1A ครับ ~