AtCoder Tenka1 Programmer Beginner Contest 参戦記

ABCやARCと違うコンテストに参加するのは今回が初でした。

KLabさん主催とのことですが、ほぼいつもの問題の感じと同じでしたね。

今度こそは水色に、と意気込んだのですが、Cの考察不足で最後までWAが残ってしまいまたもや撃沈でした。。。

A - Measure

問題

https://beta.atcoder.jp/contests/tenka1-2018-beginner/tasks/tenka1_2018_a

解法

入力文字列の長さで分岐させるだけ

実装

s=gets.chomp
if s.size == 3
    puts s.reverse
else
    puts s
end

B - Exchange

問題

https://beta.atcoder.jp/contests/tenka1-2018-beginner/tasks/tenka1_2018_b

解法

二人の操作を愚直にシミュレートしていくだけ

実装

a,b,k=gets.chomp.split.map(&:to_i)
k.times do |i|
    if i % 2 == 0
        if a%2 == 1
            a -= 1
        end
        b += a/2
        a /= 2
    else
        if b%2 == 1
            b -= 1
        end
        a += b/2
        b /= 2
    end
end

printf("%d %d\n",a,b)

C - Align

問題

https://beta.atcoder.jp/contests/tenka1-2018-beginner/tasks/tenka1_2018_c

解法

整数の数Nは 10^5 なので、全て列挙する方法だとぜんぜん間に合いません。

よってどういう並びが適切かを考えることになります。

例えば、1、3、5という3つの整数を並べる場合を考えます。

この場合、1 3 5 と3つを昇順に並べるのは、1 5 という並びと等価です。

なぜならば、1 3 5 は (5-3) + (3-1) = 4 であり、1 5は (5-1) = 4 となるため、真ん中に3を入れる意味が無くなります。これは降順についても同じです。

よって、並べ方のうち3つ以上が昇順(または降順)で現れるものは最適ではありません。

そのため、作る列をsで表すとすると、 s_1 \geq s_2 \leq s_3 \geq s_4 .... s_N のような凸凹の並びになります。

この際、整数の数が偶数か奇数かで分けて考える必要があります。

そこで実際にNが偶数の場合として6を考えてみます。

すると、 s_1 \leq s_2 \geq s_3 \leq s_4 \geq s_5 \leq s_6 s_1 \geq s_2 \leq s_3 \geq s_4 \leq s_5 \geq s_6 が考えられます。

それぞれの計算結果は

 2 \times s_2 + 2 \times s_4 + s_6 - 2 \times  s_3 - 2 \times s_5 - s_1

 2 \times s_3 + 2 \times s_5 + s_1 - 2 \times  s_2 - 2 \times s_4 - s_6

となりますが、両者は並び順が異なるだけなので同じとみなせます。

前者を最大にすることを考えた場合、2倍で加算される s_2 s_4にはなるべく大きい値、2倍で減算されるs_3s_5にはなるべく小さい値を当てはめるべきです。

よって、事前に与えられた整数をソートしておき、大きい方の半分は2倍して加算、小さい方の半分は2倍して減算していけばよいことがわかります。ただし、中央値の2つの数については1倍のままにする必要があります。

次にNが奇数の場合として5を考えてみます。

今度は、 s_1 \leq s_2 \geq s_3 \leq s_4 \geq s_5 s_1 \geq s_2 \leq s_3 \geq s_4 \leq s_5 が考えられます。

それぞれの計算結果は

 2 \times s_2 + 2 \times s_4 - 2 \times  s_3 - s_5 - s_1

 2 \times s_3 + s_5 + s_1 - 2 \times  s_2 - 2 \times s_4

となり、 両者は異なります。これは、並びの両端に大きい数を配置するか小さい数を配置するかの違いになります。(本番では2パターンあることに気づかずに片方だけ実装してしまい見事にWAをくらいました。。。)

よって、奇数の場合は両方のケースを計算してみて、結果が大きい方を解とすればよいです。

実装

n=gets.to_i
a = []
n.times do
    a << gets.to_i
end
a.sort!

ans = 0
if n % 2 == 0
    (n/2+1).upto(n-1) do |i|
        ans += a[i] * 2    
    end

    ans += a[n/2]
    (0).upto(n/2-2) do |i|
        ans -= a[i] * 2    
    end
    ans -= a[n/2-1]
else
    (n/2+1).upto(n-1) do |i|
        ans += a[i] * 2    
    end

    (0).upto(n/2-2) do |i|
        ans -= a[i] * 2    
    end
    ans -= a[n/2]
    ans -= a[n/2-1]

    tmp = ans
    ans = 0

    (n/2+2).upto(n-1) do |i|
        ans += a[i] * 2    
    end
    ans += a[n/2]
    ans += a[n/2+1]

    (0).upto(n/2-1) do |i|
        ans -= a[i] * 2    
    end

    ans = tmp if tmp > ans

end
puts ans

D - Crossing

問題

https://beta.atcoder.jp/contests/tenka1-2018-beginner/tasks/tenka1_2018_d

解法

満たすべき条件の対称性や入力例1の結果を見る限り、部分集合に含まれる整数の数は全て等しいと予想されます。

そこで、試しに部分集合のサイズが3になるケースを考えてみます。

すると部分集合の組は {1, 2, 3} {1, 4, 5} {2, 4, 6} {3, 5, 6} となります。この場合、Nは6になります。

さらにサイズが4になるケースを考えてみます。

{1, 2, 3, 4} {1, 5, 6, 7} {2, 5, 8, 9} {3, 6, 8, 10} {4, 7, 9,10} となります。この場合、Nは10になります。

ここで、部分集合の組の数kは部分集合のサイズより1大きい点に気づくかどうかがポイントです。

例えば {1, 2, 3, 4}を基点に考えた場合、他の部分集合との積は必ず1つしかなく、かつ、各整数は各部分集合のうち2つにしか含まれないため、他の部分集合は4つしか作れません。

どの整数も部分集合の組の2つにしか含まれないため、 N = k \times (k-1) \div 2 となります。

よって、この条件を満たさないNが与えられた場合は、解は NO となります。

では次に、具体的な組み合わせを列挙する方法ですが、本番ではここでつまずいてしまいました。。。

以下のように考えます。

 S_i \cap S_j を考えた場合、これは必ずユニークな値になります。

よって、iとjを2重ループでまわして組み合わせを列挙していき、そこに整数を1つずつ当てはめていきます。その際に、各部分集合にその整数を割り当てていき、最後に各部分集合に含まれる値を出力すればよいです。

実装

n=gets.to_i

k = 0
1.upto(1000) do |i|
    if i * (i-1) / 2 == n
        k = i
        break
    elsif i * (i-1) / 2 > n
        break
    end
end

if k == 0
    puts "No"
    exit
else
    puts "Yes"
end

s = 1
ans = []
1.upto(k-1) do |i|
    (i+1).upto(k) do |j|
        ans[i] = [] if ans[i] == nil
        ans[j] = [] if ans[j] == nil
        ans[i] << s
        ans[j] << s
        s += 1
    end
end
puts k
1.upto(k) do |i|
    printf("%d ", ans[i].size)
    puts ans[i].join(" ")
end

感想

400点のCはほぼ解法がわかっていたのに、もう一歩考察が足りずにWAとなってしまいました。

500点のDも考え方はあっていたのですが、組み合わせの列挙で躓いてしまった。

ということで、またしても水色手前で足踏み状態です。

f:id:taka_xyz:20181029152757p:plain

AtCoder BC073 D: joisino's travel

問題

https://beta.atcoder.jp/contests/abc073/tasks/abc073_d

解法

訪れる町の数が最大で8のため、訪れる順番の組み合わせは最大でも8!(=40320)通りです。

町の数は最大200であり、事前にワーシャルフロイド法(計算量はO(N3))等でそれぞれの町の間の最短距離を求めておけば、全ての組み合わせを試しても十分に間に合います。

実装

#include<iostream>
#include<vector>
#include<map>
#include<algorithm>
#include<cmath>
#include<string>

#define FOR(i,a,b) for(int i=(a);i<(b);++i)
#define REP(i,n)   FOR(i,0,n)
#define MOD 1000000007

using namespace std;
 
typedef long long int ll;

const ll INF=(ll)1e18;

ll d[201][201];

int main(){
    int N, M, R;
    cin >> N >> M >> R;
    vector<int> r;

    REP(i,R){
        int tmp;
        cin >> tmp;
        r.push_back(tmp);
    }
    
    sort(r.begin(),r.end());

    REP(i,N+1)REP(j,N+1)d[i][j] = INF;

    REP(i,M){
        int a,b,c;
        cin >> a >> b >>c;
        d[a][b] = c;
        d[b][a] = c;
    }


    REP(i,N+1)REP(j,N+1)REP(k,N+1){
        d[j][k] = min(d[j][k], d[j][i] + d[i][k]);
    }

    ll ans = INF;
    do {
        ll tmp = 0;
        REP(i,R-1){
            tmp += d[r[i]][r[i+1]];
        }
        ans = min(ans,tmp);
    } while (std::next_permutation(r.begin(), r.end()));

    cout << ans << endl;

}

AtCoder BC073 C: Write and Erase

問題

https://beta.atcoder.jp/contests/abc073/tasks/abc073_c

解法

数字の出現回数が

・偶数回:消えている ・奇数回:書かれている

ので、mapを使って出現回数をカウントしておき、最後に奇数回のものを数えれば解けます。

これ以外にもsetを使って操作をシミュレートしても良い。

実装

#include<iostream>
#include<vector>
#include<map>
#include<algorithm>
#include<cmath>
#include<string>

#define FOR(i,a,b) for(int i=(a);i<(b);++i)
#define REP(i,n)   FOR(i,0,n)
#define MOD 1000000007

using namespace std;
 
typedef long long int ll;

const ll INF=(ll)1e18;

int main(){
    int N;
    cin >> N;

    map<ll,int> m;

    REP(i,N){
        ll a;
        cin >> a;
        m[a]++;
    }

    int ans=0;
    for(auto itr = m.begin(); itr != m.end(); itr++){
        if(itr->second % 2 != 0){
            ans++;
        }
    }

    cout << ans << endl;
}

radikoのエリアフリーを保存する方法

以前、radikoのタイムフリーを保存する方法を書きましたが、今回はエリアフリー対応版です。

takaxyz.hatenablog.com

基本的な手順は同じですが、異なるのは最初にログイン処理を行ってCookieをローカルに保存し、以降はそのCookieを送信する点です。

環境

OS: Mac OS High Sierra 必要なソフト:swftoolsffmpeg (Homebrewより導入)

事前準備

myplayer-release.swfのダウンロード

認証のための事前準備としてswfファイルをダウンロードする

$ curl -O http://radiko.jp/apps/js/flash/myplayer-release.swf

swfファイルから画像ファイルを取り出す

認証にキーとなる画像ファイルをswfextractで取り出す

$ swfextract myplayer-release.swf -b 12 -o authkey.png
$ ls authkey.png
authkey.png

実行

ログイン処理

エリアフリーの番組を保存する場合、この手順が必要になります。

最後のmailとpassにプレミア会員登録しているメールアドレスとパスワードを設定します。

$ curl -c cookie.txt 'https://radiko.jp/ap/member/login/login' \
-XPOST \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'Referer: http://radiko.jp/' \
-H 'Pragma: no-cache' \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.1 Safari/603.1.30' \
-H 'X-Radiko-Device: pc' \
-H 'X-Radiko-App-Version: 4.0.0' \
-H 'X-Radiko-User: test-stream' \
-H 'X-Radiko-App: pc_ts' \
--data 'mail=aaaa@example.com&pass=password'

実行すると、cookie.txtにクッキーが保存されます。

以降のcurlコマンドではこのファイルを-bオプションで指定します。

認証1

認証1のURLにアクセスし、HTTPのレスポンスヘッダーから以下の3つの値を取得する

・X-Radiko-AuthToken ・X-Radiko-KeyLength ・X-Radiko-KeyOffset

$ curl -b cookie.txt 'https://radiko.jp/v2/api/auth1_fms' \
> -XPOST \
> -H 'Content-Type: application/x-www-form-urlencoded' \
> -H 'Referer: http://radiko.jp/' \
> -H 'Pragma: no-cache' \
> -H 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1 Safari/605.1.15' \
> -H 'X-Radiko-Device: pc' \
> -H 'X-Radiko-App-Version: 4.0.0' \
> -H 'X-Radiko-User: test-stream' \
> -H 'X-Radiko-App: pc_ts' \
> --data $'\r\n'
X-Radiko-AppType=pc
X-Radiko-AppType2=pc
X-Radiko-AuthToken=AJReLqXV-Oo4ymYpgRlhow
X-Radiko-AuthWait=0
X-Radiko-Delay=15
X-Radiko-KeyLength=16
X-Radiko-KeyOffset=121394

please send a part of key

事前準備でダウンロードしたキーとなる画像ファイルから、X-Radiko-KeyOffsetがオフセット、X-Radiko-KeyLengthが長さとなるデータを抽出する。

UNIXのddコマンドを使って抽出する。なお、抽出したデータはBase64で文字列に変換する。

$ dd if=authkey.png ibs=1 skip=121394 count=16 | base64
16+0 records in
0+1 records out
16 bytes transferred in 0.000669 secs (23916 bytes/sec)
kwMYHYGpTwcgn8OlMjBjyQ==

「kwMYHYGpTwcgn8OlMjBjyQ==」が認証2で使うParticalkeyとなる

認証2

認証2のURLに対して、AuthTokenとParticalkeyをPOSTする

curl -b cookie.txt 'https://radiko.jp/v2/api/auth2_fms' \
-XPOST \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'Referer: http://radiko.jp/' \
-H 'Pragma: no-cache' \
-H 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1 Safari/605.1.15' \
-H 'X-Radiko-Device: pc' \
-H 'X-Radiko-Authtoken: AJReLqXV-Oo4ymYpgRlhow' \
-H 'X-Radiko-App-Version: 4.0.0' \
-H 'X-Radiko-User: test-stream' \
-H 'X-Radiko-Partialkey: kwMYHYGpTwcgn8OlMjBjyQ==' \
-H 'X-Radiko-App: pc_ts'

JP13,東京都,tokyo Japan

応答として地域の情報が帰ってくると認証は成功。

プレイリストの取得と音声の保存

プレイリスト取得用のURL(https://radiko.jp/v2/api/ts/playlist.m3u8)に対して、クエリとして放送局IDと開始、終了時間を指定してするとプレイリストを取得できます。

なお、lというクエリには15を指定するらしいのですが、意味は不明です。

放送局IDについてはググると出てくると思います。

curl -b cookie.txt 'https://radiko.jp/v2/api/ts/playlist.m3u8?station_id=MBS&l=15&ft=20181013222500&to=20181013235500' \
-XPOST \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'Referer: http://radiko.jp/' \
-H 'Pragma: no-cache' \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1 Safari/605.1.15' \
-H 'X-Radiko-Authtoken: AJReLqXV-Oo4ymYpgRlhow' \
--data 'flash=1'

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=52973,CODECS="mp4a.40.5"
https://radiko.jp/v2/api/ts/chunklist/GTgzFQEx.m3u8

これをffmpegの引数として渡すことで、音声の保存が可能です。

なお、ffmpegの実行時はCookieを送信する必要はありません。

ffmpeg \
> -content_type 'application/x-www-form-urlencoded' \
> -headers 'Referer: http://radiko.jp/' \
> -headers 'Pragma: no-cache' \
> -headers 'X-Radiko-AuthToken: AJReLqXV-Oo4ymYpgRlhow' \
-user_agent 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1 Safari/605.1.15' \
> -i 'https://radiko.jp/v2/api/ts/chunklist/GTgzFQEx.m3u8' \
> -vn -acodec copy -bsf aac_adtstoasc out.m4a

iTunes等で聴けるように、ffmpegのオプションでm4a形式で保存するようにしています。

まとめ

以上の手順によってradikoのエリアフリーを保存することが出来ます。

AtCoder BC072 D: Derangement

問題

https://beta.atcoder.jp/contests/abc072/tasks/arc082_b

解法

左から順にpiをみていきます。

・pi ≠ iの時 何もせずに、右に1つ進む

・pi = iの時 次の数と入れ替えて、右に2つ進む。このとき、次の数(pi+1)がi+1かどうかは特に意識する必要は無い。どちらの場合も、入れ替え後に条件を満たすため。

 なお、数列の右端がpi = iの場合は、一つ前の数と入れ替えればOK。

以上です。

ちょっと400点とは思えないほど簡単です。

実装

#include<iostream>

#define FOR(i,a,b) for(int i=(a);i<(b);++i)
#define REP(i,n)   FOR(i,0,n)
#define MOD 1000000007

using namespace std;
 
typedef long long int ll;

const ll INF=(ll)1e18;

int main(){
    int N;
    cin >> N;

    int p[N];

    REP(i,N)cin >> p[i];

    int i = 0;
    int ans = 0;
    while(i < N){
        if(p[i] == i + 1){
            ans += 1;
            i += 2;
        }else{
            i++;
        }
    }

    cout << ans << endl;
}

AtCoder BC072 C: Together

問題

https://beta.atcoder.jp/contests/abc072/tasks/arc082_a

解法

aiがとりうる値は、ai-1、ai、ai+1の3つです。

なのでとりうる値の数をカウントする配列を用意しておき、各aiについてai-1、ai、ai+1の3カ所を1ずつ加算していけば、その最大値が解となります。

実装

aiが0となる場合の扱いを簡略化するため、aiと+1、+2の箇所を加算している。最終的な解は「個数の最大値」であるためこのままでも問題無い。

#include<iostream>
#include<vector>
#include<map>
#include<algorithm>
#include<cmath>
#include<string>
 
#define FOR(i,a,b) for(int i=(a);i<(b);++i)
#define REP(i,n)   FOR(i,0,n)
#define MOD 1000000007
 
using namespace std;
 
typedef long long int ll;
 
const ll INF=(ll)1e18;
 
#define MAX 99999
 
int s[MAX+3];
 
int main(){
    int N;
    cin >> N;
 
    REP(i,MAX+3)s[i]=0;
 
    REP(i,N){
        int a;
        cin >> a;
 
 
        s[a]++;
        s[a+1]++;
        s[a+2]++;
 
    }
 
    int ans = 0;
 
    REP(i,MAX+3)ans = max(ans, s[i]);
 
    cout << ans << endl;
}

AtCoder BC 071 D: Coloring Dominoes

問題

https://beta.atcoder.jp/contests/abc071/tasks/arc081_b

解法

左から順にドミノの並びを見ていく場合、並び方は

・縦に1つ置く ・横に2つ重ねる

の2パターンです。

そこで、直前の並びに対する次の並びでの塗り方のパターンを考えます。

・「縦に1つ」→ 「縦に1つ」  直前の色以外の2色が選べる = 2通り

・「縦に1つ」→ 「横に2つ」  直前の色以外の2色について、どちらかを上、どちらかを下にする = 2通り

・「横に2つ」→ 「縦に1つ」  必ず直前の色以外の1色を選ぶことになる = 1通り

・「横に2つ」→ 「横に2つ」  上に直前の上下の2色以外を選ぶ場合、下は必ず直前の上の色になる = 1通り  上に直前の下の色を選ぶ場合、下は残りの2色を選べる = 2通り  合わせて3通りとなる

また、一番左側のドミノについては、

・「縦に1つ」= 3通り ・「横に2つ」= 3 x 2 = 6通り

となる。

よって、左側から順番にドミノの並びを検査していき、上記のパターンをかけていくと解ける。(毎回MODをとることを忘れない)

実装

#include<iostream>
#include<algorithm>
#include<string>

#define FOR(i,a,b) for(int i=(a);i<(b);++i)
#define REP(i,n)   FOR(i,0,n)
#define MOD 1000000007

using namespace std;
 
typedef long long int ll;

const ll INF=(ll)1e18;

int main(){
    int N;
    cin >> N;

    string s1, s2;

    cin >> s1 >> s2;

    int i = 0;

    ll ans = 0;
    int flag;
    if(s1[i] == s2[i]){
        ans = 3;
        i += 1;
        flag = 1;
    }else{
        ans = 6;
        i += 2;
        flag = 2;
    }

    while(i < N){
        if(s1[i] == s2[i]){
            if(flag == 1){
                ans *= 2;
                ans %= MOD;
            }
            i += 1;
            flag = 1;
        }else{
            if(flag == 1){
                ans *= 2;
                ans %= MOD;
            }else{
                ans *= 3;
                ans %= MOD;
            }
            i += 2;
            flag = 2;
        }
    }

    cout << ans << endl;
}