들어가며

Qualification 라운드에서 “파싱을 FSM으로 짜볼까?” 같은 이상한 생각을 하고 그렇게 풀었는데(잘못된 선택이었다), 이번 라운드에서도 “요즘 Rust 배우는 중인데 Rust로 짜볼까?” 같은 이상한 생각을 하고 그렇게 풀었다(역시 잘못된 선택이었다). 대신 Rust 숙련도가 기대했던 것보다 많이 늘었다. 이번 라운드는 Rust의 패배가 아니라 러폭도 콰즈의 패배다…

Rust 좋고 튼튼한 언어긴 한데, 확실히 C++에 비해 코드가 장황한 점은 있었다. 해커컵 치기 전에 BOJ에서 몇 문제 풀고 왔는데 남들 500 바이트 나오는 코드가 2500 바이트 나왔다. Rust로 더럽게(모든 루틴을 main에 넣는다든지) 짜면 더 줄일 수 있을 것 같긴 한데, 일단은 Rust로 PS를 계속 하려면 여러 스타일을 다양하게 테스트 해 볼 필요성이 있다고 느꼈다.

이게 다 Rust 때문이고 C++로 풀었으면 더 잘했을 거거든요! – 정신승리를 시전중

풀이

Pie Progress

각 날짜별로 파이 가격을 sorting하고, 세금까지 계산해서 파이를 k개 샀을 때 돈을 얼마나 내야 하는지를 계산한다. 다음으로 각 날짜 i가 지났을 때 파이 j개가 남도록 할 때, 최적의 전략을 선택했을 때 소모하는 금액을 DP를 이용해 계산한다.

pie_progress.rs
use std::fmt::Debug;
use std::str::{FromStr, SplitWhitespace};

trait Parser {
    fn read<T, E>(&mut self) -> T
        where T: FromStr<Err = E>,
              E: Debug;
}

impl<'a> Parser for SplitWhitespace<'a> {
    fn read<T, E>(&mut self) -> T
        where T: FromStr<Err = E>,
              E: Debug {
        self.next().expect("End of file")
            .parse::<T>().expect("Parse Error")
    }
}

//==============================================================================
use std::cmp::min;
use std::io;
use std::io::Read;

struct Input {
    num_day: i32,
    num_pie: i32,
    price: Vec<Vec<i32>>,
}

fn read_input(iter: &mut SplitWhitespace) -> Input {
    let num_day: i32 = iter.read();
    let num_pie: i32 = iter.read();
    let mut price = Vec::new();

    price.push(vec!());
    for _ in 0..num_day {
        let mut day_price = Vec::with_capacity(num_pie as usize);
        for _ in 0..num_pie {
            day_price.push(iter.read());
        }
        price.push(day_price);
    }

    Input {
        num_day: num_day,
        num_pie: num_pie,
        price: price,
    }
}

fn solve(input: Input) -> i32 {
    let num_day = input.num_day;
    let price = input.price;

    let day_size = (num_day+1) as usize;
    let mut dp = vec!(vec!(std::i32::MAX; day_size); day_size);

    dp[0][1] = 0;

    for day in 1usize..day_size {
        let mut today_price = price[day].clone();
        today_price.sort();

        let mut cost = Vec::with_capacity(day_size);
        cost.push(0);
        for (i, pie_price) in today_price.iter().enumerate() {
            let current_cost = cost[i] + pie_price + 2*(i as i32) + 1;
            cost.push(current_cost);
        }

        for stored_pie in 1usize..day_size {
            let prev_price = dp[day-1][stored_pie];
            if prev_price == std::i32::MAX { break }
            for (i, pie_cost) in cost.iter().enumerate() {
                if stored_pie-1+i >= day_size { break }
                let cell = &mut dp[day][(stored_pie-1+i) as usize];
                *cell = min(*cell, prev_price + pie_cost);
            }
        }
    }

    dp[num_day as usize][1]
}

fn main() {
    let mut buffer = String::new();
    io::stdin().read_to_string(&mut buffer).expect("Failed to read input");

    let mut iter = buffer.split_whitespace();

    let num_case: i32 = iter.read();

    for now_case in 1..(num_case+1) {
        let input = read_input(&mut iter);
        println!("Case #{}: {}", now_case, solve(input));
    }
}

Manic Moving

플로이드-와셜 알고리즘으로 각 도시들 사이의 최단 경로를 구해 두고, 1차원 DP를 돌렸다. i번째 사람의 이사 계획이 s[i]에서 e[i]로 가는 것이라고 하면, 내가 시작할 수 있는 상태는 두 가지가 있다.

  1. s[i]에서 시작(i-1번째 사람의 짐을 내려놓고 그 다음 i번째 사람의 짐을 들러 왔다)
  2. e[i-1]에서 시작(i-1번째 사람의 짐을 나르는 도중 i번째 사람의 짐을 들었다)

각 두 가지 시작 케이스에 대해 (current) -> e[i] -> s[i+1]인 경우와 (current) -> s[i+1] -> e[i]인 경우 두 가지를 처리하면 된다.

근데 틀렸다. 왜 틀렸지?

(추가) 제보를 받고 틀린 부분을 찾았다. 중복 간선이 있어서 floyd[edge.from][edge.to] = edge.gas; 이렇게 하면 안 되고 min을 씌워 주어야 한다.

manic_moving.rs
use std::fmt::Debug;
use std::str::{FromStr, SplitWhitespace};

trait Parser {
    fn read<T, E>(&mut self) -> T
        where T: FromStr<Err = E>,
              E: Debug;
}

impl<'a> Parser for SplitWhitespace<'a> {
    fn read<T, E>(&mut self) -> T
        where T: FromStr<Err = E>,
              E: Debug {
        self.next().expect("End of file")
            .parse::<T>().expect("Parse Error")
    }
}

//==============================================================================
use std::cmp::min;
use std::io;
use std::io::Read;

#[derive(Clone)]
struct Edge {
    from: usize,
    to: usize,
    gas: i32,
}

struct Customer {
    start: usize,
    dest: usize,
}

struct Input {
    num_city: usize,
    graph: Vec<Vec<Edge>>,
    customers: Vec<Customer>,
}

fn read_input(iter: &mut SplitWhitespace) -> Input {
    let num_city = iter.read();
    let num_edge = iter.read();
    let num_customers = iter.read();

    let city_size = (num_city+1) as usize;
    let mut graph = vec![vec!(); city_size];
    for _ in 1..city_size { graph.push(Vec::new()); }

    for _ in 0..num_edge {
        let a: usize = iter.read();
        let b: usize = iter.read();
        let gas: i32 = iter.read();

        graph[a].push(Edge {
            from: a,
            to: b,
            gas: gas,
        });
        graph[b].push(Edge {
            from: b,
            to: a,
            gas: gas,
        });
    }

    let mut customers = Vec::new();

    for _ in 0..num_customers {
        customers.push(Customer {
            start: iter.read(), dest: iter.read()
        })
    }

    Input {
        num_city: num_city,
        graph: graph,
        customers: customers,
    }
}

const INF: i32 = 7_0000_0000;

fn solve(input: Input) -> i32 {
    let customers = &input.customers;

    let city_size = (input.num_city+1) as usize;
    let customer_size = customers.len();

    let mut floyd = vec!(vec!(INF; city_size); city_size);
    for i in 1..city_size { floyd[i][i] = 0; }
    for v in input.graph {
        for edge in v {
            floyd[edge.from][edge.to] = edge.gas;
        }
    }
    for k in 1..city_size {
        for i in 1..city_size {
            for j in 1..city_size {
                if floyd[i][j] > floyd[i][k]+floyd[k][j] {
                    floyd[i][j] = floyd[i][k]+floyd[k][j];
                }
            }
        }
    }

    let mut load = vec![INF; customer_size];
    let mut preload = vec![INF; customer_size];

    let mut last_preload = 0;

    load[0] = floyd[1][input.customers[0].start];

    let last_index = input.customers.len()-1;
    for (i, customer) in input.customers.iter().enumerate() {
        if i == last_index { break }

        let next_start = input.customers[i+1].start;
        load[i+1] = min(
            load[i] + floyd[customer.start][customer.dest] + floyd[customer.dest][next_start],
            preload[i] + floyd[last_preload][customer.dest] + floyd[customer.dest][next_start],
        );

        preload[i+1] = min(
            load[i] + floyd[customer.start][next_start] + floyd[next_start][customer.dest],
            preload[i] + floyd[last_preload][next_start] + floyd[next_start][customer.dest],
        );

        if load[i+1] >= INF && preload[i+1] >= INF {
            return -1;
        }

        last_preload = customer.dest;
    }

    let customer = &customers[last_index];
    let ans = min(
        load[last_index] + floyd[customer.start][customer.dest],
        preload[last_index] + floyd[last_preload][customer.dest],
    );

    if ans == INF { -1 } else { ans }
}

fn main() {
    let mut buffer = String::new();
    io::stdin().read_to_string(&mut buffer).expect("Failed to read input");

    let mut iter = buffer.split_whitespace();

    let num_case: i32 = iter.read();

    for now_case in 1..(num_case+1) {
        let input = read_input(&mut iter);
        println!("Case #{}: {}", now_case, solve(input));
    }
}

Fighting the Zombies

리넘버링 해서 O(N^4)으로 사각형 두 개를 잡아서 루프를 돌며 세는 문제라고 생각했으나, 원 영역에 애매하게 걸치는 바람에 이동 후 사각형 밖에 존재하게 되어 버리는 점이 생기지 않는지를 엄밀하게 검증하기 싫어서 Beach Umbrellas 문제로 넘어갔다. 맞은 분들 솔루션 보니까 처음 생각한대로 풀면 되는 것 같다. 대회 끝나고 후기를 퇴고하면서 항상 가능하다는 걸 보였는데, 원 반지름을 무한으로 보내서 사각형이 겹치지 않는 경우 그 사이에 선을 긋고(여기까진 대회 도중 생각했다), 겹치는 경우에는 교점 두 개를 대상으로 선을 그어 공간을 분할하면 항상 사각형 두 개를 고르는 경우로 치환 가능하다는 걸 알게 되었다.

Beach Umbrellas

가장 끝에 들어갈 우산 두 개를 루프 돌리고, 나머지 빈 칸에 대해 중복조합과 순열을 이용해 열심히 계산하면 된다. 수식으로는 다음과 같고, modular inverse를 써서 계산한 뒤 2\times(n-2)!을 곱해 차례로 더하면 된다.

\,_{n+1}H_{free\_spot} = \,_{free\_spot+n}C_{free\_spot} = \,_{free\_spot+n}C_n

내 코드 시간복잡도가 O(N^2)인줄 알고 input 파일을 다운 받았는데 알고보니 O(N^3)이라서 망한 문제다. 최적화 하려면 할 수는 있는 부분이었지만 6분만에 익숙하지 않은 언어로 고치기는 불가능했다. 다행히 6분은 꽤 길기 때문에 시간 내에 답이 나올듯 말듯 진행됐는데, 30초쯤 남았을 때 95번 정도고 마지막에 최대 크기 데이터가 몰려 있는걸 보고 포기하고 코드 전시라도 하자 싶어서 그 때까지의 출력을 복사한 다음 뒤쪽을 다 0으로 채운 fuck.txt를 제출했다. 그런데 7초 남기고 100번까지 결과가 다 나왔다! 하지만 파일 포커스가 fuck.txt에 맞춰져 있어서 output.txt로 바꿔야 했고, Rust는 Working Directory의 src 폴더 안에 소스가 들어 있어서 output.txtmain.rs가 들어 있는 디렉터리가 달라 제출 할 때 폴더 이동이 필요했다. 결국 클릭이 2초쯤 느려서 제출하지 못했다. 맞은 분들 코드 보니까 O(N^3)도 종종 보이는 걸 보고 아쉬웠다.

Expire 되고 나서 20분쯤 남았는데, 제출한 게 다 맞아야 R2를 진출한다는 부담감이 있었지만 20분 안에 한 문제를 못 풀 것 같아서 씻으러 갔다. 씻고 왔더니 3번이 틀려 있었고 R2의 꿈은 저편으로…

beach_umbrellas.rs
use std::fmt::Debug;
use std::str::{FromStr, SplitWhitespace};

trait Parser {
    fn read<T, E>(&mut self) -> T
        where T: FromStr<Err = E>,
              E: Debug;
}

impl<'a> Parser for SplitWhitespace<'a> {
    fn read<T, E>(&mut self) -> T
        where T: FromStr<Err = E>,
              E: Debug {
        self.next().expect("End of file")
            .parse::<T>().expect("Parse Error")
    }
}

//==============================================================================
use std::io;
use std::io::Read;

struct Input {
    num_spot: i64,
    radius: Vec<i64>,
}

fn read_input(iter: &mut SplitWhitespace) -> Input {
    let num_umbrella = iter.read();
    let num_spot = iter.read();
    let mut radius = Vec::new();

    for _ in 0..num_umbrella {
        radius.push(iter.read());
    }

    Input {
        num_spot: num_spot,
        radius: radius,
    }
}

const MOD: i64 = 1_000_000_007;

fn modinv(val: i64) -> i64 {
    let (mut a, mut b) = (val, MOD);
    let (mut x0, mut x1) = (0, 1);
    while a > 1 {
        let q = a / b;

        let t = b;
        b = a%b;
        a = t;

        let t = x0;
        x0 = x1 - q*x0;
        x1 = t;
    }
    if x1 < 0 { x1 += MOD };
    x1
}

fn solve(input: Input) -> i64 {
    let n = input.radius.len();
    let radius = &input.radius;
    if n == 1 {
        return (input.num_spot as i64) % MOD;
    }

    let radius_sum = radius.iter().fold(0, |acc, v| acc+v*2) as i64;

    let ordering = (1i64..(n-1) as i64).fold(1i64, |acc, v| acc*v%MOD);
    let inversed = (1i64..(n+1) as i64).fold(1i64, |acc, v| acc*modinv(v)%MOD);

    let mut ans = 0i64;
    for left in 0..n {
        for right in left+1..n {
            let radius_sum = radius_sum - radius[left] - radius[right];
            let free_spot = input.num_spot-1 - radius_sum;

            if free_spot < 0 {
                continue;
            }

            // n+1 H free_spot = free_spot+n C free_spot = free_spot+n C n
            let mut delta = ordering * inversed % MOD;
            for i in 0i64..n as i64 {
                delta = delta * (free_spot+n as i64 - i) % MOD;
            }
            ans = (ans + delta*2) % MOD;
        }
    }

    ans % MOD
}

fn main() {
    let mut buffer = String::new();
    io::stdin().read_to_string(&mut buffer).expect("Failed to read input");

    let mut iter = buffer.split_whitespace();

    let num_case: i32 = iter.read();

    for now_case in 1..(num_case+1) {
        let input = read_input(&mut iter);
        println!("Case #{}: {}", now_case, solve(input));
    }
}

OpenCV와 Android Studio 버전에 따라 다른 내용이 생길 수 있으니 주의 바랍니다.

1. 안드로이드 SDK 다운로드

OpenCV 다운로드 페이지에서 OpenCV for Android를 다운 받습니다. 다운 받은 파일을 원하는 위치에 압축 해제합니다. 저는 편리한 접근을 위해 C 드라이브에 바로 압축해제 했습니다.

2. 프로젝트 생성

새롭게 프로젝트를 생성합니다. 안드로이드에서 OpenCV를 사용할 때는 OpenCV JAVA API를 사용할 수도 있고, OpenCV Native API를 JNI 형태로 사용할 수도 있습니다. 후자가 지원하는 기능이 더 많지만 추가로 설정이 필요합니다. 이 문서에서는 JNI를 사용하는 부분까지 전부 다룰 예정이지만 JAVA API 부분만 사용할 예정이라면 C++ 지원은 해제하셔도 됩니다.

다음으로 최소 SDK 버전 설정 페이지가 나오는데, API 21 이상으로 설정합니다. OpenCV 3.1 버전에서는 안드로이드의 camera2 클래스를 사용하기 때문에 최소 SDK 버전을 21 이상으로 맞출 필요가 있습니다. 이후는 기존의 안드로이드 프로젝트 생성과 동일합니다. Empty Activity를 선택했고, C++11 지원을 켠 것 이외에는 모두 기본 설정으로 진행했습니다.

CMake, NDK 등 JNI 사용에 필요한 도구들이 설치되어 있지 않아 에러 메시지가 발생하는 경우 Tools > Android > SDK Manager의 SDK Tools 탭에서 설치할 수 있습니다. 설치 후에 build.gradle 파일을 열어 프로젝트를 동기화 합니다.

3. OpenCV JAVA API 사용하기

File > New > Import Module을 클릭해 모듈을 추가합니다. Source Directory에 <OpenCV Path>/sdk/java를 입력하면 모듈 이름을 자동으로 감지합니다.

다음으로, OpenCVLibrary310/build.gradle 파일을 열어 compileSdkVersion과 buildToolsVersion을 app/build.gradle 파일과 동일하게 맞춥니다. minSdkVersion도 21로 조정합니다. 최종적으로 수정된 제 build.gradle 파일은 다음과 같으며, 사용하는 안드로이드 컴파일 SDK 버전에 따라 수치가 다를 수 있습니다. 수정 후 gradle 파일 sync 버튼을 눌러 빌드가 되는지를 확인합니다.

apply plugin: 'com.android.library'

android {
    compileSdkVersion 24
    buildToolsVersion "24.0.2"

    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 21
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
        }
    }
}

다음으로는 모듈 사이의 의존성을 설정해 주어야 합니다. 프로젝트 탐색기에서 프로젝트 폴더를 우클릭하고, Open Module Settings를 클릭한 뒤, app 모듈을 선택하고 Module Dependency로 openCVLibrary310을 추가합니다.

마지막으로, JAVA API 로드가 성공적으로 되었는지를 확인하기 위해 다음 코드를 Main Activity에 추가합니다.

    private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {
        @Override
        public void onManagerConnected(int status) {
            switch (status) {
                case LoaderCallbackInterface.SUCCESS:
                {
                    Log.i("OpenCV", "OpenCV loaded successfully");
                } break;
                default:
                {
                    super.onManagerConnected(status);
                } break;
            }
        }
    };

    @Override
    public void onResume()
    {
        super.onResume();
        OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_3_1_0, this, mLoaderCallback);
    }

빌드 후 디바이스에서 실행하려고 하면, OpenCV Manager가 설치되어 있지 않다는 메시지와 함께 구글 플레이 스토어로 이동 됩니다. 그대로 설치하면 되고, 에뮬레이터 등 플레이 스토어가 존재하지 않는 환경에서는 <OpenCV Path>/apk에서 CPU 아키텍처에 맞는 apk 파일을 이용해 설치할 수도 있습니다. 이렇게 사용하는 경우 OpenCV를 사용하는 애플리케이션끼리 라이브러리 하나를 서비스 형태로 공유해서 사용할 수 있으며, OpenCV 홈페이지에서 권장하고 있는 방법입니다.

OpenCV Manager 없이 애플리케이션을 standalone 형태로 배포하고 싶은 경우에는 <OpenCV Path>/sdk/native/libs에 있는 라이브러리 파일을 사용해야 하며 여기서는 다루지 않겠습니다. OpenCV 3.1 튜토리얼Static Initialization 부분을 참고하시면 됩니다.

여기까지 완료하셨다면 빌드 후 디바이스에 설치한 뒤, Android Monitor 기능을 이용해 성공적으로 OpenCV API를 호출하는 것을 확인할 수 있습니다.

4. OpenCV JNI로 사용하기

OpenCV의 JAVA API에도 유용한 기능이 많지만, 퍼포먼스가 C++ Native Code보다 떨어지고 지원하는 기능이 적습니다. 이 때 JNI를 이용하면 기존의 C++ OpenCV API를 안드로이드 자바 코드에서 호출해 사용할 수 있습니다.

기존 튜토리얼 대다수가 안드로이드 스튜디오 하위 버전을 기준으로 작성되었고, Deprecated된 기능을 사용하고 있어 JNI 빌드 부분에서 고생을 했습니다. 구글의 안드로이드 스튜디오 가이드가 큰 도움이 되었습니다.

프로젝트를 생성할 때 C++ 지원을 활성화 하셨다면 CMakeLists.txt와 함께 native-lib.cpp 파일이 생성되셨을 겁니다. CMakeLists.txt에 다음 내용을 추가합니다. OpenCV Path는 자신의 설치 경로로 바꿔주시기 바랍니다.

set(OpenCV_DIR <OpenCV Path>/sdk/native/jni)
find_package(OpenCV REQUIRED)

그리고 target_link_libraries의 마지막에 ${OpenCV_LIBS}를 추가합니다. IDE 동기화가 성공적으로 이루어지는지 확인합니다.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context="io.qwaz.androidopencv.MainActivity">

    <TextView
        android:id="@+id/sample_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!" />

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:srcCompat="@android:color/transparent"
        android:layout_below="@+id/sample_text"
        android:id="@+id/imageView"
        android:layout_alignParentStart="true"
        android:adjustViewBounds="false"
        android:cropToPadding="false"
        android:layout_marginTop="16dp" />
</RelativeLayout>
#include <jni.h>
#include <string>
#include <opencv2/opencv.hpp>

extern "C" {

JNIEXPORT jstring JNICALL
Java_io_qwaz_androidopencv_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

JNIEXPORT void JNICALL
Java_io_qwaz_androidopencv_MainActivity_fillMatrixColor(
        JNIEnv* env,
        jobject,
        jlong addrMat,
        jint red,
        jint green,
        jint blue
) {
    using namespace cv;

    Mat &mat = *(Mat*)addrMat;
    mat = Scalar(red, green, blue);

    return;
}

}
package io.qwaz.androidopencv;

import android.graphics.Bitmap;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.ImageView;
import android.widget.TextView;

import org.opencv.android.BaseLoaderCallback;
import org.opencv.android.LoaderCallbackInterface;
import org.opencv.android.OpenCVLoader;
import org.opencv.android.Utils;
import org.opencv.core.CvType;
import org.opencv.core.Mat;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Example of a call to a native method
        TextView tv = (TextView) findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());
    }

    private BaseLoaderCallback mLoaderCallback = new BaseLoaderCallback(this) {
        @Override
        public void onManagerConnected(int status) {
            switch (status) {
                case LoaderCallbackInterface.SUCCESS:
                {
                    Log.i("OpenCV", "OpenCV loaded successfully");
                    // OpenCV 초기화 이후 함수 호출
                    updateImageView();
                } break;
                default:
                {
                    super.onManagerConnected(status);
                } break;
            }
        }
    };

    @Override
    public void onResume()
    {
        super.onResume();
        OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_3_1_0, this, mLoaderCallback);
    }

    private void updateImageView() {
        ImageView iv = (ImageView) findViewById(R.id.imageView);
        iv.getImageMatrix();

        Mat mat = Mat.zeros(iv.getHeight(), iv.getWidth(), CvType.CV_8UC3);
        fillMatrixColor(mat.getNativeObjAddr(), 200, 1, 80);

        Bitmap bmp = Bitmap.createBitmap(mat.cols(), mat.rows(), Bitmap.Config.ARGB_8888);
        Utils.matToBitmap(mat, bmp);

        iv.setImageBitmap(bmp);
    }

    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();
    private native void fillMatrixColor(long addrMat, int red, int green, int blue);

    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }
}

screenshot_2016-10-16-16-48-29

위와 같이 ImageView에 색깔이 입혀진 것을 확인할 수 있습니다.

방학 중간에 쓰기 시작한 글인데 귀찮다고 미루다 보니 쓸 책도 많아지고 내용도 많이 잊어버렸다.

서민의 기생충 콘서트

착한 기생충, 특이한 기생충, 나쁜 기생충으로 기생충을 분류해 재밌게 설명하는 책이다. 서민 씨는 책에서도 그렇고 인터뷰에서도 그렇고 “기생충은 우리의 친구다!”를 일관성 있게 주장하시는 분인데, ‘배탈을 일으키긴 하지만 세상에 치사율이 50%가 넘는 기생충도 있는데 이 정도면 착한 기생충이다’라는 이야기가 처음엔 공감 가지 않았지만, 책을 읽다 보니 점점 세뇌되는 기분이었다. 나쁜 기생충 챕터에는 코로 들어가 뇌를 파괴하는 등 진짜 무서운 기생충들이 많이 나오는데, 읽을 땐 무서웠지만 지금 생각해보면 내가 에볼라 바이러스를 두려워하지 않는 것과 마찬가지로 별로 신경 쓰지 않아도 되는 생물들인 것 같다. 기생충에 대한 생물학적 소개뿐 아니라 에피소드를 다양하게 실었던 점이 마음에 들었다.

다크 존

꾸준히 이어지는 기시 유스케 덕질의 일환. 이제 읽을 책이 [자물쇠가 잠긴 방] 한 권 남았는데, 도서관에서 책이 분실되는 바람에 못 읽고 있다가 얼마 전 예약 도서 찾아가라고 연락이 와서 빌려서 읽고 있다. 기시 유스케 다 읽은 다음에는 베르나르 베르베르 책을 읽어볼까 생각 중이다.

작가 본인이 일본장기 팬이라서 장기 소재를 소재로 종종 가져다 쓰는데, [다크 존]도 그런 작품 중 하나다. 다크 존이라는 정체불명의 공간에서 참가자들이 각자 하나의 말을 맡아 서바이벌 게임을 하는 장면과 현실에서 벌어지는 일들을 절묘하게 교차 편집해서 이야기를 진행한다. [해리포터와 마법사의 돌] 후반부에 나오는 마법사 체스를 장편소설화 시킨 느낌이라 생각하면 될 것 같다. 일본장기에서 아이디어를 가져온 부분은 있지만, 관련 지식이 없어도 재미있게 읽을 수 있도록 주석이 자세하게 달려 있다. [크림슨의 미궁]도 같은 서바이벌 호러 장르였는데, 아무래도 [다크 존]이 좀 더 이후 작품이다 보니 스토리 구성과 묘사가 더 깔끔하다는 느낌을 받았다.

퀴어이론 입문

요즘 페미니즘에 관심을 가지면서 전통적 성 역할의 변화에 대해 공부해 보려는 마음가짐으로 반납함에 있던 걸 집어 와서 읽었는데 기대만큼 취향에 맞지는 않았다. 인터넷에 돌아다니는 자칭 전문가들이 사실 별로 아는 게 없다는 걸 새삼스레 느꼈고 사회가 바뀌는 데는 역시 많은 시간이 필요하다는 생각을 했다. 상식으로 알아둘 만한 내용들도 있기는 하지만 학술적인 접근이 대부분이었고, 시간이 지나니 읽은 양에 비해 기억에 남은 내용이 별로 없다 크흑… 다루고 있던 토픽 중에 기억나는 것들을 몇 개 적어보면 성소수자를 나타내는 표현의 변천사, 성소수자들이 어떻게 사회에서 목소리를 내게 되었는지, 퀴어 커뮤니티의 변화, 레즈비언 페미니즘 등이 있다.

문제해결로 살펴본 수학사

PL 시간에 Type Theory나 수학과 프로그래밍의 연관성 등에 관해 이야기를 들었던 게 재밌어서 골랐다. 그러지 말았어야 했는데. 이것도 기대한 내용과 별로 겹치는 게 없었던 책이다. 챕터 구성은 주제 제시, 주제와 관련된 수학자의 일생, 개념 설명 및 예제 풀이, 연습 문제로 구성되어 있다. 고등학교 때 읽었던 [수학의 반전]이랑 느낌이 비슷한데, 주제에 대한 깊이가 살짝 얕고 거기에 수학자의 일생을 넣으면 비슷한 느낌이 날 것 같다. 당연히 연습 문제는 풀지 않았다.

별로 재미는 없었지만 읽기 시작한 게 아까워서 끝까지 읽었다. 비추천할 정도는 아니지만 굳이 시간 내서 읽을 필요는 없다는 느낌의 책. 앞부분은 자연수, 유리수 등의 기초적인 개념에서 시작해서 뒷부분은 해밍 코드, 리만 가설 등이 나온다. 토픽만 보면 어려운 것들도 많은데 어디 가서 아는 척할 수 있는 정도까지만 다루고 있다는 점이 장점이다.

소녀불충분

애니메이션 서브컬처 쪽에서 유명한 작가 중 한 명인 니시오 이신의 책이다. 좋아하냐 싫어하냐라고 물으면 아슬아슬하게 좋아하는 편으로 분류하는 작가지만 책을 사서 볼 정도로 좋아하지는 않는데, 기시 유스케 책 사면서 배송비 무료를 적용받으려고 한 권 끼워 샀다. 장점일지 단점일지는 모르겠지만 한 마디로 평가를 내리자면 지금까지 읽었던 니시오 이신 작품 중 가장 니시오 이신 느낌이 나는 책이었다. 말장난이 없었던 게 조금 아쉬웠을지도?

스포일러 포함
소설인 척하는 논픽션인 척하는 소설이다. 당연히 실제로는 없을 것 같은 사건을 다루고 있는데 작가가 책 안에서 “이것은 실화다”, “이게 내가 소설을 쓰게 된 계기”라고 자꾸 주장해서 헷갈렸는데 다 읽고 나니 역시 소설일 수밖에 없는 이야기라고 생각한다. 초등학생한테 납치당하는 대학생 히키코모리 작가 지망생의 이야기.

제노사이드

도서관에 오고 가고 하면서 꾸준히 대출함에 나타나는 책이길래 재밌으니까 많이 대출되겠지 싶어서 읽어 보았다. 기시 유스케의 탄탄한 과학적 고증에 대비하면 아쉬운 부분이 많았지만, 전체적인 스토리 자체는 나쁘지는 않았다. 제일 당황스러웠던 부분은 “일방향 암호로 암호화된 위성 카메라 데이터 정보(?)를 인간의 영역을 뛰어 넘는 계산 능력을 바탕으로 복호화(?)”하는 장면이었다. 화학이나 생물 쪽도 이상하다 싶은 부분이 많이 나오는데 관련 전공 분들이 보면 더 많이 찾으실 듯. 고증은 좀 문제가 있지만 과학을 하는 사람들의 심리 묘사는 공감 가는 장면이 많았다.

스토리 자체는 살짝 유치하고, 전형적인 캐릭터 설정이나 전개 방식이 많이 보였다. 작가가 친한인 것 같은데 한국인 조력자가 중요한 역할로 등장하고 한국인의 ‘정’에 관한 설명도 나와서, 일본 소설에서 저런 내용을 보게 될 줄 몰랐는데 조금 당황했다. 내용에 담긴 주제 의식은 괜찮은 편이었다고 생각하지만 베스트셀러감은 아닌 것 같다고 생각한다.

학벌사회

부제가 “사회적 주체성에 대한 철학적 탐구”다. 진짜 철학자가 철학적으로 접근해서 학벌 의식이 어떤 문제가 있는지를 분석한 책이다. 10년 전에 나온 책임에도 불구하고 지금이랑 문제점이 똑같다는 점이 참 안타깝다. 인상 깊었던 부분을 몇 개 요약해보면 다음과 같다. 인상에 남지 않은 부분에도 좋은 내용이 많아서 한 번쯤 읽기를 추천하고 싶은 책이다.

  1. 학벌은 한 번 고정되면 바꿀 수 없다는 점에서 현대판 신분 제도라고도 할 수 있다.
  2. 아무리 학벌 의식이 심한 사람이라도 외국에서는 학벌 의식이 생기지 않는다는 점에서 학벌은 한국 사회에서의 특수한 현상으로 생각해 분석할 필요가 있다.
  3. 대학은 고등 교육 기관인데, 모든 사람이 대학에 가는 분위기 때문에 대학의 정체성이 점점 더 모호해지고 있다.

작가는 대학이 평준화되어 있는 독일에서 유학을 갔다 왔는데, 그때의 개인적인 경험을 조금 성급하게 일반화한다는 느낌 빼고는 정말 공감 가는 부분이 많았던 책이다.

차회예고

요즘은 [자물쇠가 잠긴 방] 짬짬이 읽고 있다. 방범 탐정 시리즈 최신작으로 이것만 읽으면 드디어 기시 유스케 컴플리트를 찍는다. 그 외에는 전공 책을 읽고 있는데, 옥스토비 7판 생각보다 열심히 읽고 있고 고오급 프로그래밍 과제로 [Clean Code]를 읽고 있다. 클린 코드는 과제로 읽는 책치고는 상당히 재밌다. 일단 베르나르 베르베르 소설을 읽어볼까 생각 중이긴 한데 책 읽는 포스테키안 대상 도서를 먼저 읽게 될 것 같다.

2016년 SCPC(삼성 대학생 프로그래밍 경진대회)에 참가했다. 올해로 2회를 맞는 젊은 대회인데, 국내 대회임에도 불구하고 구글 코드잼 같은 유명 세계대회를 뛰어넘는 엄청난 규모의 상금으로 유명하다. 작년에는 미국에 있었고 온라인 예선도 까먹고 안 쳐서 주변에 상금 받은 사람들을 보면서 부러워 했었는데 올해는 기회가 돼서 본선에 참가했다.

대회 시작은 오후 1시였는데 대회장 내부에 간단하게 체험 부스 등이 준비되어 있어서 미리 와서 이벤트에 참가하는 것을 권장하더라. 포토존이나 간식거리, 체험 게임 등 이벤트로서의 준비는 굉장히 잘 되어 있었는데, 대회로서의 준비는 아무래도 경험이 적어서 그런지 아쉬운 부분이 눈에 띄었다. 대회장 내부에 음료 반입 금지라든지, 대회 시작 전 컴퓨터가 열려 있어서 미리 코드를 짜 놓는 방식의 부정행위가 가능할 것 같다는 걱정도 있었고, 채점은 g++로 하면서 로컬에는 MSVC만 깔려 있다든지 이것저것 있었다.

풀이

연습문제 – Lumbering

이벤트 문제로 영어 문제가 나왔다. 문제 자체는 간단했는데 가장 먼저 푼 사람에게만 상품을 주기 때문에 이것 때문에 괜히 빨리 풀어야 한다는 생각에 사로잡혀서 그냥 푸는 것보다 오래 걸렸다. (자르는 높이 – 자르는 시간)이 가장 작은 도끼질 시간을 찾아서, 모든 나무를 그 도끼질 한 번으로 자른다고 생각하면 된다. 처음에는 스택이나 인덱스트리 쪽으로 생각했는데 그럴 필요가 전혀 없었다.

A – 재활용

Simple DP. 시작 위치와 끝 위치를 고정했을 때 쓰레기통을 하나 놓아서 거리가 최소화 되게 하는 지점을 O(N^2)로 전처리 한 뒤 이 배열을 사용해 O(N^2) DP를 돌리면 된다. 예선 2차 1번에서 N = 5000 제한 조건으로 같은 O(N^2)라도 커팅을 해야만 통과가 되도록 시간제한을 약간 빡빡하게 줬었던 기억이 나서 최대한 불필요한 테이블 계산을 줄이려고 노력했다. long long을 쓰지 않아 한 번 WA를 받았다.

B – 랩뮤직

B에서 삽질을 좀 오래 한 편이다. 전날 했던 벼락치기의 부작용 때문에 생각이 어려운 방향으로 흘러가서 Suffix Array도 짜고 LCP도 구하다가 깨달음을 얻고 BFS + KMP로 빠르게 풀었다. 반복문을 N까지 돌아야 되는데 N-1까지 돌아서 틀리는 등의 실수도 좀 해서 시간이 상당히 걸렸다.

C – 폭격

B를 풀고 시간이 두 시간 약간 넘게 남았다. C랑 D를 읽어봤는데 D는 생각도 어렵고 코드 짜는 게 너무 오래 걸릴 것이라 판단돼서 C를 잡았다. KOI 기출인 두부 모판 자르기랑 비슷한 문제인데 비트 DP가 아니라 3진수 DP를 해야 하고, 리넘버링도 해야 하고, 그냥 돌리면 느려서 큐를 사용해 최적화를 하면 풀리는 문제다. 해야 할 것들은 명확하지만 그걸 짜기가 어려운 유형의 문제. 어떤 y가 입력됐을 때 y-1, y, y+1 셋을 모두 추가하면 리넘버링 후에도 단순 격자 위에 있다고 생각할 수 있다는 걸 관찰해 코드의 복잡도를 줄일 수 있었다.

사소한 오타 등의 실수는 있었지만 전체 코드 설계는 처음부터 부드럽게 진행된 문제였다. 이건 벼락치기의 도움을 받았다고 생각하는데, 내가 공부해간 알고리즘들을 직접적으로 쓰는 문제는 없었지만 잘 짠 예제 코드들을 보다가 가서 설계를 잘 한 게 아닐까 생각하고 있다.

풀고 나니까 시간이 20분 정도 남았는데, D 부분점수를 노리기에도 시간이 빠듯해 그냥 C 푸는 사람이 적기를 기원하며 새로고침 누르면서 시간을 때웠다. 내가 C를 6번째로 풀었는데, 마지막에 C를 푼 사람이 7명밖에 되지 않아 안심하며 3등상을 예상하고 있었다.

뒷풀이

대회 끝나고 나서는 미니 토크 하고, 경품 추첨 하고, 설문조사 등을 하다가 시상식을 했다. 미니 토크에서는 시간제한이 적다고 말이 많았던 2차 3번에 대해 교수님이 풀이해 주셨는데, 말이 많았던 이유가 같은 O(N lg N) 풀이임에도 불구하고 통과가 되는 풀이가 있고 아닌 풀이가 있었기 때문이지만 “호호 여러분들 엔제곱 풀이를 생각하셨나 봐요” 같은 투로 말씀하셔서 여기서도 대회측이 참가자들의 여론을 잘 파악하지 못하고 있다는 것을 느꼈다. 설문조사에 이런 부분을 담으려고 노력했지만 잘 전달되었는지는 모르겠다.

경품 추첨에서는 운 좋게 기어 360을 받았다. 시계나 태블릿을 원했다. 참가만 해도 기계식 키보드를 주는데다가 상금도 많고 이벤트로 뿌린 경품도 많았다. 역시 삼성은 돈이 많다. 시상식에서는 대회 때 예상한대로 4~8위에게 주는 3등상을 받았다. 대회 나가기 전 예상으로 실수를 좀 하면 5등상(~38위), 평타 치면 4등상(~18위) 정도 받겠다고 생각했는데 예상보다 좋은 성적을 거두어 기분이 좋았다. 상 등급이 하나씩 오를 때마다 상금이 최소 두 배로 뛴다. 1등상은 요새 핫하신 dotorya님이 2천만원 받아가셨다.

사진 찍고 수상자 인터뷰 하고 짐 찾아서 아는 사람들이랑 고기 먹고 끝났다. 내일은 전대프연이 있는데 아마 이 사람들 대부분이 다시 나타날 것이다.