AtCoder Begginer Contest 032

最近、ABCの難易度の低下が激しいと思ったけど、前回よりは難しくなった?

A - 高橋君と青木君の好きな数

問題概要

n以上の整数で、aでもbでも割れる最小の整数を答えよ。
http://abc032.contest.atcoder.jp/tasks/abc032_a

解説

取り敢えずaでもbでも割れるってことは求める整数はaとbの最小公倍数の倍数。

自分は互除法を使ってLCM(a,b)=a*b/GCD(a,b)で出しましたが、aとbの制限が緩いので1から順に試して、aでもbでも割り切れた時、でもOKです。

x×LCM≧nとなるxはx=ceil(n/LCM)ですが、それさえも面倒だと思ったのか、x=1から順に試していって、x×LCMがn以上になった時点でそれを出力するという初心者なコードを書きました。

#include <bits/stdc++.h>
using namespace std;
int gcd(int a,int b){
    if(a>b)    swap(a,b);
    while(a%b!=0){
        a=a%b;  swap(a,b);
    }
    return b;
}
int main(){
    int a,b;   cin>>a>>b;
    int n; cin>>n;
    int ans=a*b/gcd(a,b);
    for(int i=1;;i++){
        if(i*ans>=n){
            cout<<i*ans<<endl;
            return 0;
        }
    }
}

B - 高橋君とパスワード

問題概要

文字列s中のk文字の部分文字列の種類を答えよ。
http://abc032.contest.atcoder.jp/tasks/abc032_b

解説

文字数≦300と小さいので、最初の地点を全通り試して、setに1つずつ入れていく。最終的なsetの大きさが答え。

今回は大丈夫だが、10万文字など文字数が大きくなると、毎回文を作りなおしているとTLEするので、その時は尺取り法を用いる。k文字を越えた時点で、新しい文は左端1文字を削除してから右端1文字を追加した文字列になる。この方法でk文字の文字列を作ってもいいけど、やや面倒です。

#include <bits/stdc++.h>
using namespace std;
int main(){
    string s;   cin>>s;
    int k; cin>>k;
    set<string> ls;
    for(int i=0;i+k<=s.size();i++){
        ls.insert(s.substr(i,k));
    }
    cout<<ls.size()<<endl;
    return 0;
}

C - 列

問題概要

数列a1,a2,...,anの中で、連続する部分列の積がkを超えないように部分列を選んだ時の長さの最大値を求めよ。
http://abc032.contest.atcoder.jp/tasks/abc032_c

解説

B問題で少し触れた尺取り法を用いる問題です。a1から順にかけていき、aiでkを越えたとします。その時、aiを含む部分列にa1がいると積がkを越えるわけですから、a1は取り除いてしまいましょう。同様に左から取り除いていって、積がk以下になるまで続けます。

このとき、ai+1を含む部分列にa1はあってはいけません。a1〜aiまでの積を含むことになるので絶対にkを越えます。このように、

  • 左から順にかけていく(部分列に追加する)
  • 積がkを越えている間、左から順に割っていく(部分列から取り除く)

という操作をループで回せばOKです。

ただし、数列内に0があった場合はコーナーケース。全部かけてしまえば積は0になってkの値に関わらず答えはnです。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main(){
    ll n,k; cin>>n>>k;
    vector<ll> s(n);
    for(int i=0;i<n;i++){
        cin>>s[i];
        if(s[i]==0){
            cout<<n<<endl;
            return 0;
        }
    }
    int end=0,ans=0,cnt=0;
    ll sum=1;
    for(int i=0;i<n;i++){
        sum*=s[i];  cnt++;
        while(sum>k&&cnt>0){
            sum/=s[end];    end++;  cnt--;
        }
        ans=max(ans,cnt);
    }
    cout<<ans<<endl;
    return 0;
}

D - ナップサック問題

問題概要

0-1ナップサック問題を解け。ただし、3パターンの制限のうち少なくとも1つは満たされる。
http://abc032.contest.atcoder.jp/tasks/abc032_d

解説

この問題、3つのアルゴリズムを使い分ける問題です。具体的には、

  • N≦30のとき、半分全列挙
  • N≦200 かつ全ての i(1≦i≦N) について 1≦wi≦1000のとき、DPの添字に重さを用いる
  • N≦200 かつ全ての i(1≦i≦N) について 1≦vi≦1000のとき、DPの添字に価値を用いる

と使い分けます。因みにそれぞれの解法は全て蟻本に載っています。

半分全列挙は、全て列挙するとO(2N)でTLEしますが、O(2N/2)ならN/2≦15で間に合います。荷物を2グループに分けてその中で荷物の選び方(選ばないという選択肢もあり)を全列挙して価値の総和と重さの総和を記録しておきます(ただし、その選び方がWを越えるなら記録しなくてOK)。その後、1つ目のグループを価値の高い順、2つ目のグループを重さの軽い順に並べ、軽い方から、 * 1つ目のグループの先頭の重さ+2つ目のグループの先頭>Wである限り1つ目のグループの先頭を削除。 * 答えを1つ目のグループの先頭の価値+2つ目のグループの先頭の価値で更新できるなら更新。 * 2つ目のグループの先頭を削除。 という操作を2つ目のグループが無くなるまで実行すればOKです。 因みに自分はdeque使ってますね…(1手間無駄な気がする)

DPの添字に重さを用いる場合は重さiでの価値の最大値、価値を用いる場合は価値iでの重さの最小値を記録していきます。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll v[210],w[210];
map<ll,ll> ls[2];
ll N,W;
void brute(int i,int end,ll wsum,ll vsum,int op){
    if(i==end){
        if(wsum<=W)    ls[op][wsum]=max(ls[op][wsum],vsum);
        return;
    }
    brute(i+1,end,wsum+w[i],vsum+v[i],op);
    brute(i+1,end,wsum,vsum,op);
}

int main(){
    cin>>N>>W;
    bool flag=true;//trueでwベースのDP
    for(int i=0;i<N;i++){
        cin>>v[i]>>w[i];
        if(w[i]>1000) flag=false;//vベースのDP
    }
    if(N<=30){//半列挙
        brute(0,N/2,0,0,0);
        brute(N/2,N,0,0,1);
        ll ans=0;
        deque<pair<ll,ll>> dq;
        vector<pair<ll,ll>> vec;
        for(auto it:ls[1])  vec.push_back(it);
        reverse(vec.begin(),vec.end());//重い順に並び替え
        //重い順にdequeに突っ込んでいって価値順に並べる
        for(auto it:vec){
            while(!dq.empty()&&dq.back().second<it.second){
                dq.pop_back();
            }
            dq.push_back(it);
        }
        for(auto i:ls[0]){//ls[0]は軽い順に見る
            ll curw=i.first,curv=i.second;
            while(!dq.empty()&&curw+dq.front().first>W)    dq.pop_front();
            ans=max(ans,curv+dq.front().second);
        }
        cout<<ans<<endl;
        return 0;
    }
    else if(flag){//wベースのDP
        ll dp[N][W+1]; memset(dp,0,sizeof(dp));
        dp[0][w[0]]=v[0];
        for(int i=1;i<N;i++){
            for(int j=0;j<=W;j++){
                dp[i][j]=dp[i-1][j];
                if(j>=w[i])    dp[i][j]=max(dp[i][j],dp[i-1][j-w[i]]+v[i]);
            }
        }
        ll ans=0;
        for(int i=0;i<=W;i++)    ans=max(ans,dp[N-1][i]);
        cout<<ans<<endl;
        return 0;
    }
    else{//vベースのDP
        ll dp[200010];
        for(int i=0;i<200010;i++)   dp[i]=LLONG_MAX;
        dp[0]=0;
        ll usemax=0;
        for(int i=0;i<N;i++){
            ll tmp=usemax;
            for(int j=tmp;j>=0;j--){
                if(dp[j]==LLONG_MAX) continue;
                if(dp[j]+w[i]<=W){
                    dp[j+v[i]]=min(dp[j+v[i]],dp[j]+w[i]);
                    usemax=max(usemax,j+v[i]);
                }
            }
        }
        cout<<usemax<<endl;
        return 0;
    }
}

感想

D問題面倒だった。過去に解いててそのコードが残っていればコピペで一瞬だったのに…
でも久しぶりの半分全列挙で結構戸惑ったので練習になった。