JAG Contest 2016 Domestic F - 土地相続

jag2016-domestic.contest.atcoder.jp

本番バグらせて解けなかった悲しいね.

問題文

省略

解法(反転して表示)

基本的な方針はDPと場合分け.累積和とかである区間の値の総和がO(1)で求めれるように前処理しとく. マスは全て非負なので,空き空間は特に作って得することがないことにまあ気づく.

なので,N=1のときは,与えられた長方形全ての総和が答え.

N=2のときは,垂直もしくは水平な直線でぶった切ってみて,その2つの総和の最小値が答え.

N=3のとき,構成される長方形群はどう配置しても,必ず垂直もしくは水平に分割できる場所があるので,dp[着目長方形の左上][左下][あと何個の長方形を取り出さなければいけないかk]という大雑把な見積もりではO(k(WH)2)な状態数を扱うDPで解ける.遷移は「垂直・水平の分割位置」と「分かれた上下(or左右)の長方形にkをどう割り振るか」を全て試す.

実際の状態数は4乗にはならない.垂直もしくは水平にぶった切るごとに端としてとりうる値の自由度が増えていくのだけれど,一回分割するごとに自由度が1つ増えるだけなので,N<=3なら自由度が高々2なので,大体二乗のdpになっているはず.

問題は,N=4のケースだが,これは,水平or垂直にぶった切れる場所が1つでもあれば,N=3の時のDPと同様にすれば,状態数O(k(WWH+WHH)),遷移O(W+H)のdpで解ける. しかし,ぶった切れないときがある.どういうケースかというと,以下のようなケースである.これさえ対策すれば良い. 僕は「下限値の二分探索」+「左上の長方形の形を決め打ちすると連鎖的に他の3つの長方形の形が貪欲で定まる」みたいな感じでこのケースの最適値を求めた.人によっては二分探索使ってない人もいるらしい. しかも,時計回りのケースを回転・反転したものに対して適用することで反時計回りの法の実装の手を抜いた. f:id:kyuridenamida:20160424215047p:plain

まとめると,N<=3ならDPするだけ,N>=4なら上記の特殊ケースを試してみて大きいほうを返す. 実際の実装ではN!=4でなくともN=4と仮定して特殊ケースを試したり,W=H=200と拡張している.

ソース

#include <bits/stdc++.h>
using namespace std;
 
 
map< array<int,4>,int> memo[5];
 
int A[210][210];
int B[210][210];
 
int sum[210][210];
int H,W,N;
 
 
inline int S(int b,int a,int d,int c){
    return sum[c][d] + sum[a-1][b-1] - sum[a-1][d] - sum[c][b-1];
}
int f(const int x1,const int x2,const int y1,const int y2,const int n){
    if( n == 1 ){
        return S(x1,y1,x2,y2);
    }
    
    if( memo[n].count({x1,x2,y1,y2}) ) return memo[n][{x1,x2,y1,y2}];
    int ans = 0;
    for(int i = x1+1 ; i <= x2 ; i++){
        int sub = 0;
        
        for(int j = 1 ; j < n ; j++){
            sub = max(sub,min(f(x1,i-1,y1,y2,n-j),f(i,x2,y1,y2,j)));
        }
        ans = max(sub,ans);
    }
    for(int i = y1+1 ; i <= y2 ; i++){
        int sub = 0;
        for(int j = 1 ; j < n ; j++){
            sub = max(sub,min(f(x1,x2,y1,i-1,n-j),f(x1,x2,i,y2,j)));
        }
        ans = max(sub,ans);
    }
    return memo[n][{x1,x2,y1,y2}] = ans;
}
 
bool check(int lower){
    if( lower == 0 ) return true;
    
    for(int h1 = 1 ; h1 < H ; h1++){
        for(int w1 = 1 ; w1 < W ; w1++){
            if( S(1,1,w1,h1) < lower ) continue;
            int h2 = h1;
            while( h2 < H and S(w1+1,1,W,h2) < lower ) h2++;
            if( S(w1+1,1,W,h2) < lower ) continue;
            int w3 = w1;
            while( w3 > 1 and S(w3,h2+1,W,H) < lower ) w3--;
            if( S(w3,h2+1,W,H) < lower ) continue;
            if( S(1,h1+1,w3-1,H) < lower ) continue;
            return true;
        }
    }
    return false;
    
}

 
int special(){
    for(int i = 1 ; i <= H ; i++)
        for(int j = 1 ; j <= W ; j++)
            sum[i][j] = A[i][j];
    for(int i = 1 ; i <= H ; i++)
        for(int j = 1 ; j <= W ; j++)
            sum[i][j] += sum[i-1][j];
    for(int i = 1 ; i <= H ; i++)
        for(int j = 1 ; j <= W ; j++)
            sum[i][j] += sum[i][j-1];

    int l = 0 , r = 1e9;
    while( l != r ){
        int m = (l+r+1) / 2;
        if( check(m) ){
            l = m;
        }else{
            r = m-1;
        }
    }
    return l;
}
int main(){
    cin >> H >> W >> N;
    for(int i = 1 ; i <= H ; i++)
        for(int j = 1 ; j <= W ; j++)
            cin >> A[i][j];
    H = W = 200;
    for(int i = 1 ; i <= H ; i++)
        for(int j = 1 ; j <= W ; j++)
            sum[i][j] = A[i][j];
    for(int i = 1 ; i <= H ; i++)
        for(int j = 1 ; j <= W ; j++)
            sum[i][j] += sum[i-1][j];
    for(int i = 1 ; i <= H ; i++)
        for(int j = 1 ; j <= W ; j++)
            sum[i][j] += sum[i][j-1];
            
    int ans = f(1,W,1,H,N);

        // N = 4 の入り組んだケース対策.盤面を回転・反転して同じ手法を試してる.無駄があるかも.
    for(int __ = 0 ; __ < 2 ; __++){
        for(int _ = 0 ; _ < 4 ; _++){
            for(int i = 1 ; i <= H ; i++){
                for(int j = 1 ; j <= W ; j++){
                    B[i][j] = A[j][i];
                }
            }
            for(int i = 1 ; i <= H ; i++){
                for(int j = 1 ; j <= W ; j++){
                    A[i][j] = B[i][j];
                }
            }
            ans = max(special(),ans);
            // cout << hoge() << endl;
        }
        for(int i = 1 ; i <= H ; i++){
            for(int j = 1 ; j <= W/2 ; j++){
                swap(A[i][j],A[i][W-j+1]);
            }
        }
    }
    cout << ans << endl;
 
}

JAG Contest 2016 Domestic E - 選挙活動

jag2016-domestic.contest.atcoder.jp

問題文

省略

解法(反転して表示)

オーソドックスな幾何. 列挙して意味のある候補点を列挙する.この候補点に意味のない点が含まれていてもいいので雑に. 候補点を列挙するために,有権者から各多角形の頂点への直線を伸ばして,それを列挙してみる(見える範囲が変化する角度に対する線分を考えるとそれを列挙するのが合理的そうなことが分かる). で,得られた直線全てについて,2つの直線の交点を求めて,それを候補点とする.

候補点が求まれば,各点について,それを見ることのできる有権者の数を数えれば良い. 条件の「多角形の頂点または有権者のいる場所の座標のうち3点が同一直線状に存在することはない.」がありがたい制約で,これのおかげで幾何パートがかなり楽になっていると言っても良い. 多分実装の軽い方針で多角形と線分の交差判定をする必要があるんですが,その制約のおかげで以下のような図のケースを考えなくて良いため,楽になる. f:id:kyuridenamida:20160424212830p:plain

計算量はよく見積もってないけど多項式時間で終わるしそんなに大きくない.

ソース

#include <bits/stdc++.h>
using namespace std;
 
const double EPS = 1e-8;
const double INF = 1e12;
typedef complex<double> P;
typedef vector<P> G;
namespace std {
  bool operator < (const P& a, const P& b) {
    return real(a) != real(b) ? real(a) < real(b) : imag(a) < imag(b);
  }
}
 
double cross(const P& a, const P& b) {
  return imag(conj(a)*b);
}
 
double dot(const P& a, const P& b) {
  return real(conj(a)*b);
}
struct L : public vector<P> {
  L(const P &a, const P &b) {
    push_back(a); push_back(b);
  }
};
 
int ccw(P a, P b, P c) {
  b -= a; c -= a;
  if (cross(b, c) > 0)   return +1;     
  if (cross(b, c) < 0)   return -1;      
  if (dot(b, c) < 0)     return +2;       
  if (norm(b) < norm(c)) return -2;       
  return 0;
}
 
P crosspoint(const L &l, const L &m) {
  double A = cross(l[1] - l[0], m[1] - m[0]);
  double B = cross(l[1] - l[0], l[1] - m[0]);
  if (abs(A) < EPS && abs(B) < EPS) return m[0]; 
  if (abs(A) < EPS) assert(false); 
  return m[0] + B / A * (m[1] - m[0]);
}
 
// 以下の条件式で,0未満としているのは(ccwの返り値は-EPSなのは全く意味ない),線分が端点で接しているときは当たっていないことにしたいため.
bool intersectSS(const L &s, const L &t) {
  return ccw(s[0],s[1],t[0])*ccw(s[0],s[1],t[1]) < -EPS &&
         ccw(t[0],t[1],s[0])*ccw(t[0],t[1],s[1]) < -EPS;
}
bool intersectLL(const L &l, const L &m) {
  return abs(cross(l[1]-l[0], m[1]-m[0])) > EPS || 
         abs(cross(l[1]-l[0], m[0]-l[0])) < EPS;   
}
 
 
P in(){
    double x,y;
    cin >> x >> y;
    return P(x,y);
}
int main(){
    int N,M;
    cin >> N >> M;
    vector<L> ls;
    vector<G> gs; 
    vector<P> check;
    vector<P> look;
    for(int i = 0 ; i < N ; i++){
        int L;
        cin >> L;
        G g;
        for(int j = 0 ; j < L ; j++){
            g.push_back(in());
            check.push_back(g.back());
            look.push_back(g.back());
        }
        gs.push_back(g);
    }
    vector<P> people;
    for(int i = 0 ; i < M ; i++){
        people.push_back(in());
    }
    for(int i = 0 ; i < M ; i++){
        for(int j = 0 ; j < check.size() ; j++){
            ls.push_back(L(check[j],people[i]));
        }
    }
    
    for(int i = 0 ; i < ls.size() ; i++){
        for(int j = i + 1 ; j < ls.size() ; j++){
            if( intersectLL(ls[i],ls[j]) ){
                look.push_back(crosspoint(ls[i],ls[j]));
            }
        }
    }
    int ans = 0;
    for( auto lp : look ){
        int sub = 0;
        for(int i = 0 ; i < M ; i++){
            int f = 1;
            for(int j = 0 ; j < gs.size() ; j++){
                for(int k = 0 ; k < gs[j].size() ; k++){
                    L l(gs[j][k],gs[j][(k+1)%gs[j].size()]);
                    if( intersectSS(l,L(people[i],lp)) ) f = 0;
                }       
            }
            if( f ) sub++;
        }
        ans = max(ans,sub);
    }
    cout << ans << endl;
}

JAG Contest 2016 Domestic D - インビジブル

jag2016-domestic.contest.atcoder.jp

問題文

省略

解法(反転して表示)

よくあるゲーム木のmin-max探索,後手は差を最小化,先手は差を最大化するように実装する. 状態数がどれくらいになるのかを見積もるのが本質. 実際山札の残り枚数と,どの区間(※)がスタックに残ってるかみたいなのを考えると,状態として O(NM*(N+M)2)程度しか無い.実際はパスの回数とかどっちのターンかみたいな状態も持つんだけど,それはとても小さい定数倍だからまあ問題ない. 実装を複雑化させないために構造体とmapを用いて実装したので重いけど通る.

(※) 手札の引き方はパスを無視すれば一意で,a_1b_1a_2b_2 ,...というふうな列になるので,この列の区間を考える.

「スタックが空になってから」パスを2回が終了条件な注意しないとWAする.

ソース

#include <bits/stdc++.h>
using namespace std;
 
 
 
struct State{
    vector<int> p1,p2;
    vector< pair<int,int> > stack;
    int turn;
    int passed;
};
bool operator < (const State &a,const State &b){
    if( a.passed != b.passed ) return a.passed < b.passed;
    if( a.turn != b.turn ) return a.turn < b.turn;
    
    if( a.p1 != b.p1 ) return a.p1 < b.p1;
    if( a.p2 != b.p2 ) return a.p2 < b.p2;
    return a.stack < b.stack;
}
 
map<State,int> memo;
int dfs(const State s){
    if( s.passed == 2 ) return 0;
    if( memo.count(s) ) return memo[s];
    int ans;
    if( s.turn == 0 ){
        ans = -1e9;
        if( s.p1.size() ){
            State cpy = s;
            cpy.stack.push_back({cpy.p1.back(),s.turn});
            cpy.p1.pop_back();
            cpy.passed = 0;
            cpy.turn ^= 1;
            ans = max(dfs(cpy),ans);
        }
        {
            State cpy = s;
            int profit = 0;
            int coef1 = 1;
            int coef2 = 1;
            for(int i = cpy.stack.size() - 1 ; i >= 0 ; i--){
                if( cpy.stack[i].second != s.turn &&  cpy.stack[i].first == -1 ) coef1 = 0;
                if( cpy.stack[i].second == s.turn &&  cpy.stack[i].first == -1 ) coef2 = 0;
                
                if( cpy.stack[i].second == s.turn &&  cpy.stack[i].first != -1 )
                    profit +=  coef1 * cpy.stack[i].first;
                if( cpy.stack[i].second != s.turn &&  cpy.stack[i].first != -1 )
                    profit -=  coef2 * cpy.stack[i].first;
            }
            if( cpy.stack.size() == 0 ) cpy.passed++;
            cpy.stack.clear();
            cpy.turn ^= 1;
            ans = max(dfs(cpy)+profit,ans);
        }
    }else{
        ans = +1e9;
        if( s.p2.size() ){
            State cpy = s;
            cpy.stack.push_back({cpy.p2.back(),s.turn});
            cpy.p2.pop_back();
            cpy.passed = 0;
            cpy.turn ^= 1;
            ans = min(dfs(cpy),ans);
        }
        {
            State cpy = s;
            int profit = 0;
            int coef1 = 1;
            int coef2 = 1;
            for(int i = cpy.stack.size() - 1 ; i >= 0 ; i--){
                if( cpy.stack[i].second != s.turn &&  cpy.stack[i].first == -1 ) coef1 = 0;
                if( cpy.stack[i].second == s.turn &&  cpy.stack[i].first == -1 ) coef2 = 0;
                
                if( cpy.stack[i].second == s.turn &&  cpy.stack[i].first != -1 )
                    profit +=  coef1 * cpy.stack[i].first;
                if( cpy.stack[i].second != s.turn &&  cpy.stack[i].first != -1 )
                    profit -=  coef2 * cpy.stack[i].first;
            }
            if( cpy.stack.size() == 0 ) cpy.passed++;
            cpy.stack.clear();
            cpy.turn ^= 1;
            ans = min(dfs(cpy)-profit,ans);
            
        }
    }
    return memo[s] = ans;
    
}
 
int main(){
    int n,m;
    cin >> n >> m;
    vector<int> a(n), b(m);
    for(int i = 0 ; i < n ; i++) cin >> a[i];
    for(int i = 0 ; i < m ; i++) cin >> b[i];
    reverse(a.begin(),a.end());
    reverse(b.begin(),b.end());
    
    State s = {a,b,{},0,0};
    cout << dfs(s) << endl;
    
}

JAG Contest 2016 Domestic B - 豪邸と宅配便

jag2016-domestic.contest.atcoder.jp

問題文

省略

解法(反転して表示)

時刻 a に宅配便が届くと,[a-M,a+M)の範囲に書斎には居られない. なので,[0,T)の範囲で区間に含まれていない時刻を列挙すれば良い. 境界に気をつけつつ,愚直にシミュレーションすると時間計算量 O(NM+T) で解ける.いもす法を使うと O(N+T) になるけど制約的にどうでもいい.

ソース

#include <bits/stdc++.h>
using namespace std;
 
 
int imos[20100];
int main(){
    int N,M,T;
    cin >> N >> M >> T;
    for(int i = 0 ; i < N ; i++){
        int a;
        cin >> a;
        for(int j = 0 ; j <= M ; j++)
            if( a - j >= 0 ) imos[a-j] = true;
        for(int j = 0 ; j < M ; j++)
            if( a + j < T ) imos[a+j] = true;
    }
    
    int ok = 0;
    for(int i = 0 ; i < T ; i++){
        // cout << imos[i] << " " << i << endl;;
        if( !imos[i] ){
            ok++;
        }
    }
    cout << ok << endl;
}

JAG Contest 2016 Domestic A - 阿吽の呼吸

jag2016-domestic.contest.atcoder.jp

問題文

省略

解法(反転して表示)

本質的には括弧列が与えられるのでvalidな括弧列ですかという問題に言い換えれば見通し良く解ける. カウンタを定義する.「A」が来たらカウンタをインクリメント,「Un」が来たらデクリメント,途中でカウンタが負の数になったらNO,最後まで処理してカウント!=0ならNO.

ソース

#include <bits/stdc++.h>
using namespace std;
 
int main(){
    int a = 0;
    int n;
    cin >> n;
    for(int i = 0 ; i < n ; i++){
        string t;
        cin >> t;
        if( t == "A" ){
            a++;
        }else{
            a--;
            if( a < 0 ){
                cout << "NO" << endl;
                return 0;
            }
        }
    }
    if( a == 0 ) cout << "YES" << endl;
    else cout << "NO" << endl;
}

Python3.5 Windowsにsklearn(+scipy,numpy)を入れる

なんか面倒くさかったので,困っている人のためにメモ

そもそもWindowsはscipyのインストールが面倒くさい.↓を参考にしてお手軽にインストールした.

Windows で VirtualEnv の Python2.7 に pip と wheel を使って コンパイルエラーが発生するパッケージ(例 scipy)をWindows用バイナリ提供サイトから入手してインストールする - グロブ

それでもまだ, ImportError: DLL load failed: The specified module could not be found. が出る.困った.どうやら,numpyをインストールするときも普通のnumpyではだめらしく,numpy-MKLをインストールしないといけないらしい(この辺は詳しくない). 上の記事と同様 numpy-mklをwheel形式でインストール (Python Extension Packages for Windows - Christoph Gohlke)

stackoverflow.com

そしたら動いた.

Python3+bs4で AtCoderのスクレイピング入門(初級者向け)

正直後から思ったんですが、urllib使うよりrequestsのほうが使いやすいと思うので、requestsを使うことをおすすめします (2016/07/17)

※ python2のurllibの情報が欲しい人はurllibの仕様が2系と違うのでこの記事を参考にしても意味ないです

Python3とbs4(beautifulsoup4)でAtCoderの便利スクリプトを作る.

対象読者はWebスクレイピングしたいけど,ブラウザ以外からのログインさえできる気がしないって人です.Pythonよくしらない人向けに書いたつもりでしたが,僕もPython初心者で教え方が全くわからないのでちょっと厳しいかも.

かなり手を抜いているのでコメントか直接何かしらの手段で質問してくれたら答えたいと思います.すみません.

bs4 をインストールしよう

bs4は何をしてくれるやつかっていうと,htmlとかxmlのタグをパースしてくれるというか,必要な情報抜き出してくれるやつです. 正規表現とかガリガリ駆使しながらタグ抜き出す作業するのは楽しいけどめんどいいので,そういった面倒臭さから僕達を遠ざけてくれます. 嬉しい!

pip install beautifulsoup4

でOK.pipがない人はインストールしといてください.

AtCoderPythonからログインしよう

AtCoderPythonからログインします.

ログインするためにはどういう手順を踏めばいい?

自分のアカウントでログインしたクッキーを維持しながらサイト内をWebスクレイピング(ウェブサイトから情報を抽出すること)しないといけません.

これはPython3に標準搭載されているurllibというインターネット上のリソースを取得するためのパッケージを使うと簡単に実現できます.

ログインはどのように行われるかというと,AtCoder(を含むいろいろなサイト)はPOSTリクエストという形式でログインに必要な入力データを持ってあるURLにリクエストすると,ログイン処理を行ってくれるように出来ています.

AtCoderのログインフォーム(https://arc001.contest.atcoder.jp/login) を見ると,以下のようになっています.

  • ユーザー名の部分
<input type="text" id="name" class="input-xlarge" name="name" value="" autocomplete="off">
  • パスワードの部分
<input type="password" id="name" class="input-xlarge" name="password" value="">

name部分が重要に注目してください."name"と"password"になっていますね. これに関するデータをPOST形式で渡すときの要素名になります.

実際にログインしてみる

さっそくAtCoderにログインしてみましょう.

ディクショナリをPOSTデータの実質の形式(password=パスワード&name=ユーザー名)にしてくれる関数urllib.parse.urlencode()があるので,それを使います.

username,password = 'ユーザー名','パスワード'
postdata = {
    'name': username,
    'password': password
}
encoded_postdata = urllib.parse.urlencode(postdata).encode('utf-8')
req = opener.open("https://arc001.contest.atcoder.jp/login",encoded_postdata)

print(req.read().decode('utf-8')) # req.read()だけだとバイナリで表示されてよくわからないのでutf8の文字列に変換

ログインに成功するとトップページに飛ぶので, 表示されたソースに「パスワードを忘れた方はこちら」とかが無ければログインに成功しています. おめでとうございます.

参考にさせて頂いたサイト様: http://stackoverflow.com/questions/5010915/how-do-i-clear-the-cookies-in-urllib-request-python3

AtCoderから情報を取得してこよう

問題番号とそれに対応するリストを取得してみる

ひとたび/loginにアクセスしてログインに成功したら,openerにクッキーの情報が紐付いてくれているので,あとは好き放題リクエストしまくりです.

では,問題番号とそれに対応するURLのリストを取得してみましょう. 課題ページ(http://arc001.contest.atcoder.jp/assignments )のソースを見て,問題へのリンクがあるところを抜き出すと,以下のようになっています.

<td class="center"><a class="linkwrapper" href="/tasks/arc001_1">A</a></td>
<td><a class="linkwrapper" href="/tasks/arc001_1">センター採点</a></td>

どうやら運の良いことに問題ページのURLにリンクされている⇔classがlinkwrapperであるタグで囲まれているということが分かりますね.

soupのオブジェクトを作って,select関数で.linkwrapperクラスのタグを全列挙してみましょう.

以下のコードにドットがついているのはcssのclassだからだと思います.

req = opener.open("http://arc001.contest.atcoder.jp/assignments")
soup = BeautifulSoup(req,"html.parser")
for tag in soup.select('.linkwrapper'):
  print(tag)

実行結果は以下のようになると思います. 実際にはtagは文字列型ではなくいろいろ操作ができるオブジェクトですが,あくまでprintした時の挙動はタグそのものを出力します.

<a class="linkwrapper" href="/tasks/arc001_1">A</a>
<a class="linkwrapper" href="/tasks/arc001_1">センター採点</a>
<a class="linkwrapper" href="/tasks/arc001_2">B</a>
<a class="linkwrapper" href="/tasks/arc001_2">リモコン</a>
<a class="linkwrapper" href="/tasks/arc001_3">C</a>
<a class="linkwrapper" href="/tasks/arc001_3">パズルのお手伝い</a>
<a class="linkwrapper" href="/tasks/arc001_4">D</a>
<a class="linkwrapper" href="/tasks/arc001_4">レースゲーム</a>

1つの問題につき2つリンクがあるので,これはお好みですが0,2,4,...番目だけ取り出せばいい感じになると思います.どうせこのへん賢くやっても仕様変更されたらおしまいなので,その時点で動いてればいいんじゃないでしょうか.

せっかくなので問題番号とそれに紐付いたURLというタプルのリストを作ってみましょう. PythonはすごいのでリストXに対してX[0::2]とすると2個飛ばしでリストを抜き出してくれます. こんなかんじになります.

X = []
for tag in soup.select('.linkwrapper')[0::2]:
    problemid = tag.text
    url = "http://arc001.contest.atcoder.jp"+tag.get("href")
    X.append((problemid,url))
print(X)

ここで,tagのメンバとかメソッドでtextとかget("href")とかありますが,これはそれぞれタグの囲まれている中身やタグの属性を取得してくれる便利なやつです.

出力結果は以下の通りです.

[('A', 'http://arc001.contest.atcoder.jp/tasks/arc001_1'), ('B', 'http://arc001.contest.atcoder.jp/tasks/arc001_2'), ('C', 'http://arc001.contest.atcoder.jp/tasks/arc001_3'), ('D', 'http://arc001.contest.atcoder.jp/tasks/arc001_4')]

ところで,リスト内包表記という記法を使ってタプルのリストを作るソースが以下のようになります. こういうワンライナーな使い方をすると可読性が低い気もしますが書きやすいです.

print([(tag.text,"http://arc001.contest.atcoder.jp"+tag.get("href")) for tag in soup.select('.linkwrapper')[0::2]])

問題文からサンプルを抜き出そう

あとはやることは同じです. ソースがどういう構造になってるか眺めながら,特徴になってきそうな部分を取り出してくるだけです. どうやら入出力の枠はpreタグで表現されているみたいです.これを全て取得すれば常勝では!?

しかし,気をつけることがあります.AtCoderの問題文は作る人によってhtmlのフォーマットが微妙に違うっぽいです. 具体的な例としては,preタグを入力形式・サンプル入出力以外にも使っている作問者がいるということが挙げられます.

ちなみにpreタグについている属性も人によってまちまちなので,あまりそこを情報として使うとよくないかも.

ただまあ,大体はお行儀の良い感じになっているはずなので,取得してみてバグったら修正を繰り返してればなんとかなるはずです.

ARC001で試してみる

ARC001のA問題(http://arc001.contest.atcoder.jp/tasks/arc001_1 )に対して,preタグでフィルタをかけてみましょう.

req = opener.open("http://arc001.contest.atcoder.jp/tasks/arc001_1")
soup = BeautifulSoup(req,"html.parser")
for tag in soup.select('pre'):
  print(tag)
<pre>
<var>N</var>
<var>c_1c_2c_3…c_N</var>
</pre>
<pre class="prettyprint linenums">
9
131142143
</pre>
<pre class="prettyprint linenums">
4 1
</pre>
<pre class="prettyprint linenums">
20
12341234123412341234
</pre>
<pre class="prettyprint linenums">
5 5
</pre>
<pre class="prettyprint linenums">
4
1111
</pre>
<pre class="prettyprint linenums">
4 0
</pre>

かなりいい感じに取れましたが,入力の項が混ざっています.これを取り除くために,一例として以下の方針が考えられます.

  1. prettyprintもしくはlinenumsクラスをselectに指定する

  2. 1つ目の取得結果を問答無用で取り除く

ARC001〜ARC004の範囲ならどちらの方針でも正しく動きそうです. 必要・十分条件とどれくらいのコンテストに対して成り立ちそうかみたいなのを強く意識すると正しい選択ができるかもしれませんが,これといった正解はないです.アドホックです.

まあとりあえずそのへんどう処理するかはおいおい考えるとして, とりあえず入力と出力を切り分けてみます.2.の方針のほうが多分多くのケースを網羅できるんですが,今回は1.の方針でやってみましょう.

opener = opener.open("http://arc001.contest.atcoder.jp/tasks/arc001_1")
soup = BeautifulSoup(req,"html.parser")
tags = soup.select('.prettyprint')
input_tags = tags[0::2]
output_tags = tags[1::2]
# len(input_tags) == len(output_tags) は勝手に仮定
for i in range(len(input_tags)):
  print("---- sample %d ----" % i) # こういう記法です
  print(input_tags[i].text.strip()) # strip()は前後の改行空白消してくれるやつです
  print("--")
  print(output_tags[i].text.strip())
  print()

出力結果は以下の通りになります.

---- sample 0 ----
9
131142143
--
4 1

---- sample 1 ----
20
12341234123412341234
--
5 5

---- sample 2 ----
4
1111
--
4 0

概ねこんな感じになりました. いい感じですね.あとはこのテキストをファイルに保存したりしたら自動ダウンローダーは完成です.おめでとうございます.

ところで,前後の改行空白を消すためにstrip()を使っていますが,不具合の元になる可能性はあります.が,そんな入力作るwriterいないと思いますし,サンプルに関してはミスって無駄に改行がある回とかがある可能性もあるのでstrip()する方針は間違ってなさそうです.

おわりに

いかがでしたでしょうか. 実はダウンローダーツールを作っていて,そのついでに並行でこの記事を書いてみました. 自分が扱ってるコードとここに書いたコードはいろいろ違うので,もしなんかこの部分動かないよ!ってのがあったらコメントください.

bs4についてもっと知りたい人は以下の記事が参考になると思います.僕もこれを見ました.ありがとうございます. http://qiita.com/itkr/items/513318a9b5b92bd56185