ルクの競プロ部屋

学生の競プロ生活を見たい人はどぞ

競プロ精選 100 問 by C++<全探索:工夫して通り数を減らす全列挙>

はじめに

今回は工夫全列挙問題を5問やっていきます!
精選100問はこちら

5 AtCoder Beginner Contest 095 C - Half and Half

問題はこちらです
問題文↓

A、B、ABの3つのピザがある
ABはAとBの半分を合わせたもので、AB2つ買うとA1つとB1つにできる
A、B、ABの3つのピザの値段と、欲しいAとBのピザの個数が与えられるので、一番安くなる方法で買ったときの値段を出力せよ

ABに対していくつ買うかを考えると、A、Bそれぞれの買う個数は決まり、
ABの制約が5000以下であることからABの個数でループさせたら良い!
結果として次のコードになる

#include <bits/stdc++.h>
using namespace std;
#define rep(i, n) for (ll i = 0; i < (n); ++i)
#define all(a) (a).begin(), (a).end()
#define rall(a) (a).rbegin(), (a).rend()
#define yesNo(b) ((b) ? "Yes" : "No")
using ll = long long;
using ull = unsigned long long;
string alphabet = "abcdefghijklmnopqrstuvwxyz";
string ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const double pi = 3.141592653589793;
int smallMOD = 998244353;
int bigMOD = 1000000007;

int main()
{
    // 入力.
    int a, b, c, x, y;
    cin >> a >> b >> c >> x >> y;

    // 結果変数.
    int ans = INT32_MAX;
    for (int i = 0; i <= max(x, y); i++)
    {
        // i個買うABの値段.
        int priceAB = i * c * 2;

        // 残りすべてのAとBをそのまま買う値段.
        int priceA = max(0, x - i) * a;
        int priceB = max(0, y - i) * b;
        ans = min(ans, priceAB + priceA + priceB);
    }

    // 結果出力.
    cout << ans << endl;
    return 0;
}

6 三井住友信託銀行プログラミングコンテスト 2019 D - Lucky PIN

問題はこちらです
問題文↓

ある文字列Sが与えられる
Sの中の文字を長さが3になるまで消していく
そこで、3文字の長さのSをパスワードにしたい
さて、何種類のパスワードが作成できるか
ただし、Sの長さは最大300000で大きい

一見Sのa番目・b番目・c番目と取り出したいが、それではO(N³)かかり間に合わない!
そこで、パスワードの長さに注目すると、000~999までの1000通りなので、これがすべて作成できるかを探索していけばいい
これだと、1000×N回の計算量で解くことが出来る!
結果として、以下のようになった

#include <bits/stdc++.h>
using namespace std;
#define rep(i, n) for (ll i = 0; i < (n); ++i)
#define all(a) (a).begin(), (a).end()
#define rall(a) (a).rbegin(), (a).rend()
#define yesNo(b) ((b) ? "Yes" : "No")
using ll = long long;
using ull = unsigned long long;
string alphabet = "abcdefghijklmnopqrstuvwxyz";
string ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const double pi = 3.141592653589793;
int smallMOD = 998244353;
int bigMOD = 1000000007;

int main()
{
    int n;
    string s;
    cin >> n >> s;
    int ans = 0;
    rep(i, 1000)
    {
        // stringstreamで0埋めを実行し文字列化.
        stringstream ss;
        ss << setw(3) << setfill('0') << i;
        string subS = ss.str();

        // 数字は作れるか.
        int scene = 0;
        rep(j, n)
        {
            if (scene == 0 && s[j] == subS[0])
            {
                scene++;
            }
            else if (scene == 1 && s[j] == subS[1])
            {
                scene++;
            }
            else if (scene == 2 && s[j] == subS[2])
            {
                scene++;
            }
        }

        // 数字は作れるか.
        if (scene == 3)
            ans++;
    }
    cout << ans << endl;
    return 0;
}

7 JOI 2007 本選 3 - 最古の遺跡

問題はこちらです
問題文↓

遺跡にはいくつかの柱がある
柱を結んで正方形になるかを求め、作成可能な正方形の面積の最大値を求めてください

普通に考えると、O(N⁴)と遅くなる!
このとき、制約は5000以下なので、一般的なコンピュータでも「5000⁴/1e9/3600/24=7.2」なので、約7日間かかる
次に考えると、1つの点がなくても四角形は組み立てられるので、O(N³)に抑えられるが、これでも「5000³/1e9/60=2.08」なので、約2分かかる
更に考えると、作成したい図形は正方形なので頂点が2つあれば、作成することが可能!
よって、O(N²)で「5000²/1e9=0.025」となり、約25msで解くことが可能になる
つまり、ここでの全探索は2つの頂点の組み合わせということになる
結果として、以下のコードになった

#include <bits/stdc++.h>
using namespace std;
#define rep(i, n) for (ll i = 0; i < (n); ++i)
#define all(a) (a).begin(), (a).end()
#define rall(a) (a).rbegin(), (a).rend()
#define yesNo(b) ((b) ? "Yes" : "No")
using ll = long long;
using ull = unsigned long long;
string alphabet = "abcdefghijklmnopqrstuvwxyz";
string ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const double pi = 3.141592653589793;
int smallMOD = 998244353;
int bigMOD = 1000000007;

int main()
{
    ll n;
    cin >> n;
    vector<vector<bool>> M(6000, vector<bool>(6000, false));
    vector<pair<ll, ll>> P;
    rep(i, n)
    {
        ll x, y;
        cin >> x >> y;
        M[y][x] = true;
        P.push_back(pair(y, x));
    }
    ll ans = 0;
    rep(i, n)
    {
        rep(j, n - i - 1)
        {
            // 2つの点が決まると、正方形を書くことが可能.
            pair<ll, ll> p1 = P[i], p2 = P[j + i + 1], p3, p4;
            p3.first = p1.first + (p2.second - p1.second);
            p3.second = p1.second + (p1.first - p2.first);
            p4.first = p2.first + (p2.second - p1.second);
            p4.second = p2.second + (p1.first - p2.first);

            // マップからはみ出していないのなら.
            if (p3.first >= 0 && p3.first <= 5500 && p3.second >= 0 && p3.second <= 5500 && p4.first >= 0 && p4.first <= 5500 && p4.second >= 0 && p4.second <= 5500)
            {
                // 作成した2つの点の上に柱が存在すれば.
                if (M[p3.first][p3.second] && M[p4.first][p4.second])
                {
                    ans = max(ans, (p2.second - p1.second) * (p2.second - p1.second) + (p1.first - p2.first) * (p1.first - p2.first));
                }
            }
        }
    }
    cout << ans << endl;
    return 0;
}

8 Square869120Contest #6 B - AtCoder Markets

問題はこちらです
問題文↓

数直線があって、人がa_iからb_iまで行きたがっている
全員の行きたい場所までにかかる距離が最小になるように入口と出口をつけてあげてください
ただし、a・bは10⁹程度あり、これを全探索するのは不可能です

a・bで全探索できたら簡単にAC取れるけど、そうはいきません
では、入力例1を考えてみます

3
5 7
2 6
8 10

入口を5、出口を7にすると、
客1:(5-5)+(7-5)+(7-7)=2移動しなきゃだめ
客2:(5-2)+(6-2)+(7-6)=8移動しなきゃだめ
客3:(8-5)+(10-8)+(10-7)=8移動しなきゃだめ
足すと18移動することになります
これを、文字的に見て、入口をs、出口をgとすると、|s-a|+|b-a|+|g-b|となる
これを客ごとに分解して考えると、
①|s-a₁|+|s-a₂|+|s-a₃|+...+|s-aₙ|
②|b₁-a₁|+|b₂-a₂|+|b₃-a₃|+...+|bₙ-aₙ|
③|g-b₁|+|g-b₂|+|g-b₃|+...+|g-bₙ|
②の式はs,gによって変わることがないので、簡単に求めることが出来る
しかし、①と③はs,gによって変わるので最適な値を出すのが難しい!
でも実は、①の式の最小値は、sがaの中央値、gがbの中央値になるらしいです
"らしい"ということで思いつかなかったということですね
(けんちょんさんの記事がわかりやすい!)
ていうことで実装は以下のようになりました

#include <bits/stdc++.h>
using namespace std;
#define rep(i, n) for (ll i = 0; i < (n); ++i)
#define all(a) (a).begin(), (a).end()
#define rall(a) (a).rbegin(), (a).rend()
#define yesNo(b) ((b) ? "Yes" : "No")
using ll = long long;
using ull = unsigned long long;
string alphabet = "abcdefghijklmnopqrstuvwxyz";
string ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const double pi = 3.141592653589793;
int smallMOD = 998244353;
int bigMOD = 1000000007;

int main()
{
    ll n;
    cin >> n;
    vector<ll> A(n), B(n);
    rep(i, n) cin >> A[i] >> B[i];

    // ②の式の値を求める.
    ll ans = 0;
    rep(i, n)
    {
        ans += abs(B[i] - A[i]);
    }

    // ①と③の式の値を求める.
    sort(all(A));
    sort(all(B));
    ll s = A[(A.size() - 1) / 2];
    ll g = B[(B.size() - 1) / 2];
    rep(i, n)
    {
        ans += abs(s - A[i]);
    }
    rep(i, n)
    {
        ans += abs(g - B[i]);
    }
    cout << ans << endl;
    return 0;
}

難しい、、、

9 JOI 2008 予選 4 - 星座探し

問題はこちらです
問題文↓

現在の星空と、星座に含まれる星の座標が与えられる
星座の座標からxとy座標をいくつ平行移動させれば現在の星空の星座と重ねることができるか
ただし、星座は現在の星空に1つしか存在しないとする

時間制限が10secかつn<1000、m<200なので現在の星座の星を全探索すれば良い!
すべての星を探索・星座の星を探索・星が存在するかの探索でO(mn²)でクリアできるので、最大10002*200/1e9=0.2秒=200ms程度でクリアできる
結果として以下のコードになる

#include <bits/stdc++.h>
using namespace std;
#define rep(i, n) for (ll i = 0; i < (n); ++i)
#define all(a) (a).begin(), (a).end()
#define rall(a) (a).rbegin(), (a).rend()
#define yesNo(b) ((b) ? "Yes" : "No")
using ll = long long;
using ull = unsigned long long;
string alphabet = "abcdefghijklmnopqrstuvwxyz";
string ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const double pi = 3.141592653589793;
int smallMOD = 998244353;
int bigMOD = 1000000007;

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

    // 星座の取得.
    vector<pair<ll, ll>> S(m);
    rep(i, m) cin >> S[i].first >> S[i].second;

    // 星空の取得.
    int n;
    cin >> n;
    vector<pair<ll, ll>> P(n);
    rep(i, n) cin >> P[i].first >> P[i].second;

    // ソートする.
    sort(all(S));
    sort(all(P));

    // 星座の間隔.
    vector<pair<ll, ll>> M;
    pair<ll, ll> c = S[0];
    rep(i, m - 1)
    {
        M.push_back(pair(S[i + 1].first - c.first, S[i + 1].second - c.second));
    }

    // 現在の星空の全探索.
    rep(i, n)
    {
        // 基準の星.
        pair<ll, ll> s = P[i];
        ll hitCount = 0;
        rep(j, m - 1)
        {
            // 星aが存在するか.
            ll ax = s.first + M[j].first;
            ll ay = s.second + M[j].second;
            rep(k, n)
            {
                if (P[k].first == ax && P[k].second == ay)
                {
                    hitCount++;
                }
            }
            // 全部hitだったら.
            if (hitCount == m - 1)
            {
                cout << P[i].first - S[0].first << " " << P[i].second - S[0].second << endl;
            }
        }
    }
    return 0;
}

全探索の典型なので、実装力が求められる問題でした
また、二分探索をしたら星の存在の探索がO(n)→O(log n)となるので、O(mn²)→O(mn log n)になり2ms程度でクリア出来るらしいです!

終わりに

約10分の1終了!
では、また来週!

ABC339A

問題のリンクはこちら

問題文

文字列が与えられるので、"."で分割したときの最後の要素を出力してください

解法

python

Pythonだと、split関数なるものがあるので、splitしてから添字-1を取得することでACとなります

s=input()
print(s.split(".")[-1])

atcoder.jp

C++

C++では、文字列のsplitが無いのでfor文で回して1文字ずつ見ていくことが有用です
また、split関数をあらかじめライブラリとして登録しておくことで、楽に解くことが出来るかもしれません
こちらが、c++のsplit関数です

void split(vector<string> &elems, const string &s, char delim)
{
    stringstream ss(s);
    string item;
    while (getline(ss, item, delim))
    {
        if (!item.empty())
            elems.push_back(item);
    }
}

こちらを参考にしました
これを使用することで以下のようになりました

#include <bits/stdc++.h>
using namespace std;
#define rep(i, n) for (ll i = 0; i < (n); ++i)
#define all(a) (a).begin(), (a).end()
#define rall(a) (a).rbegin(), (a).rend()
#define yesNo(b) ((b) ? "Yes" : "No")
using ll = long long;
using ull = unsigned long long;
string alphabet = "abcdefghijklmnopqrstuvwxyz";
string ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const double pi = 3.141592653589793;
int smallMOD = 998244353;
int bigMOD = 1000000007;

void split(vector<string> &elems, const string &s, char delim)
{
    stringstream ss(s);
    string item;
    while (getline(ss, item, delim))
    {
        if (!item.empty())
            elems.push_back(item);
    }
}
int main()
{
    int a, b;
    string s;
    cin >> s;
    vector<string> elem;
    split(elem, s, '.');
    cout << elem.back() << endl;
    return 0;
}

atcoder.jp

scratch(おまけ)

なんとなく、scratchでも解いてみました
1文字ずつループしてみて、もし"."があれば変数の上書きで対応しました

scratchでの解法
https://scratch.mit.edu/projects/960773760/

競プロ精選 100 問 by C++<全探索:全列挙編>

はじめに...

こんにちは~
この記事は初心者向けのものとなっております
あの有名な精選100を分野別に解いていきます
今回は全探索:全列挙の4問です

1 ITP1_7_B - How Many Ways?

問題サイトはこちらです!
問題文↓

1 から n までの数の中から、重複無しで3つの数を選びそれらの合計が x となる組み合わせの数を求めるプログラムを作成して下さい。

ループするものの制約が3≦n≦100かつ、数字の重複がない場合なので、普通に全探索すればいいですね
こちらが作成したコードです!

#include <bits/stdc++.h>
using namespace std;
#define rep(i, n) for (ll i = 0; i < (n); ++i)
#define all(a) (a).begin(), (a).end()
#define rall(a) (a).rbegin(), (a).rend()
#define yesNo(b) ((b) ? "Yes" : "No")
using ll = long long;
using ull = unsigned long long;
string alphabet = "abcdefghijklmnopqrstuvwxyz";
string ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const double pi = 3.141592653589793;
int smallMOD = 998244353;
int bigMOD = 1000000007;

int main()
{
    // 0 0が来るまでループさせて全探索.
    while (1)
    {
        int n = 1, x = 1;
        cin >> n >> x;
        ll ans = 0;
        if (n == 0 && x == 0)
            break;
        for (int i = 1; i <= n; i++)
        {
            for (int j = i + 1; j <= n; j++)
            {
                for (int k = j + 1; k <= n; k++)
                {
                    if (i + j + k == x)
                        ans++;
                }
            }
        }
        cout << ans << endl;
    }
    return 0;
}

入力例の受け取り個数が少し特殊かもですね。。。

2 AtCoder Beginner Contest 106 B - 105

問題サイトはこちらです!
問題文↓

1 以上 N 以下の奇数のうち, 正の約数を ちょうど 8 個持つようなものの個数を求めよ.

制約が1≦n≦200なので、最大200個の数字の約数の個数を求めればいいですね
約数の個数の求め方としては、ある整数nを素因数分解し、
n=p₁ⁿ‐¹×p₂ⁿ‐²×p₃ⁿ‐³・・・×pₐⁿ‐ⁿとすると、
求める約数は(n_1+1)(n_2+1)(n_3+1)・・・(n_n+1)となります
つまり、素因数分解してでてきた数字の指数+1をかけ合わせたものですね
結果としてこのようなコードになりました

#include <bits/stdc++.h>
using namespace std;
#define rep(i, n) for (ll i = 0; i < (n); ++i)
#define all(a) (a).begin(), (a).end()
#define rall(a) (a).rbegin(), (a).rend()
#define yesNo(b) ((b) ? "Yes" : "No")
using ll = long long;
using ull = unsigned long long;
string alphabet = "abcdefghijklmnopqrstuvwxyz";
string ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const double pi = 3.141592653589793;
int smallMOD = 998244353;
int bigMOD = 1000000007;

vector<int> primes = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199};
// 約数の個数を調べる関数.
void count(int n, int &output)
{
    vector<int> A;
    for (auto &p : primes)
    {
        A.push_back(0);
        while (n % p == 0)
        {
            n /= p;
            A.back()++;
        }
    }
    for (auto &a : A)
    {
        output *= (a + 1);
    }
}

int main()
{
    int n;
    cin >> n;
    ll ans = 0;
    // 1-nまでを全探索.
    rep(i, n)
    {
        if ((i + 1) % 2 == 1)
        {
            int o = 1;
            count(i + 1, o);
            if (o == 8)
            {
                ans++;
            }
        }
    }
    cout << ans << endl;
    return 0;
}

ちなみに、解説だと約数を列挙してました(知りません)

3 AtCoder Beginner Contest 122 B - ATCoder

問題サイトはこちらです!
問題文↓

ある文字列Sが与えられるので、Sの部分文字列のうち、ACGTの文字以外を含まない文字列の最大長を求めてください

制約を見ると、Sの長さは10以下であることがわかるので、最大、1~10までの和で55個の文字列を調べたら終了です
よって以下のようになりました

#include <bits/stdc++.h>
using namespace std;
#define rep(i, n) for (ll i = 0; i < (n); ++i)
#define all(a) (a).begin(), (a).end()
#define rall(a) (a).rbegin(), (a).rend()
#define yesNo(b) ((b) ? "Yes" : "No")
using ll = long long;
using ull = unsigned long long;
string alphabet = "abcdefghijklmnopqrstuvwxyz";
string ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const double pi = 3.141592653589793;
int smallMOD = 998244353;
int bigMOD = 1000000007;

// ACGT以外の文字が含まれていないかどうかを調べる関数.
bool isACGT(string s)
{
    bool flag = true;
    for (int i = 0; i < s.size(); i++)
    {
        if (s[i] != 'A' && s[i] != 'C' && s[i] != 'G' && s[i] != 'T')
        {
            flag = false;
        }
    }
    return flag;
}

int main()
{
    string s;
    cin >> s;
    int ans = 0;
    for (int i = 0; i < s.size(); i++)
    {
        for (int j = 0; j < s.size() - i + 1; j++)
        {
            // 文字列を切り出して全探索.
            if (isACGT(s.substr(i, j)))
            {
                ans = max(ans, (int)s.substr(i, j).size());
            }
        }
    }
    cout << ans << endl;
    return 0;
}

そのままやれば大丈夫だと思います!

4 パ研杯2019 C - カラオケ

問題サイトはこちらです!

1,2,...,N と番号づけられている N 人の生徒から成るグループが,「全国統一カラオケコンテスト」に出場することとなりました.
このコンテストで歌える曲は,曲 1,曲 2,...,曲 M の M 曲あります.
また,番号 i の生徒が曲 j を歌うと,必ず A_i,j​  点を取ります.
さて,コンテストのルールは,以下のようになります.
M 曲の中から 2 つの曲を選ぶ.(それぞれ T_1  と T_2​  とする.)
それぞれの生徒が,曲 T_1  と曲 T_2の両方を歌う.
各生徒の得点は,その生徒が歌った 2 つの曲の点数のうち高い方となる.
グループの得点は,生徒 1,2,...,N の得点の合計となる.
そのとき,グループの得点として考えられる最大の値を求めてください.

この問題は、制約が最大100ととても小さいので、全探索で大丈夫そうです
入力例2

3 4
37 29 70 41
85 69 76 50
53 10 95 100

この入力例を考えると、横向きに見るのが、M²、縦向きに見るのがNなので、O(M²N)となり、最大で約10⁶回のループで済みます
よってコードは以下のようになりました

#include <bits/stdc++.h>
using namespace std;
#define rep(i, n) for (ll i = 0; i < (n); ++i)
#define all(a) (a).begin(), (a).end()
#define rall(a) (a).rbegin(), (a).rend()
#define yesNo(b) ((b) ? "Yes" : "No")
using ll = long long;
using ull = unsigned long long;
string alphabet = "abcdefghijklmnopqrstuvwxyz";
string ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const double pi = 3.141592653589793;
int smallMOD = 998244353;
int bigMOD = 1000000007;

int main()
{
    int n, m;
    cin >> n >> m;
    vector<vector<ll>> A(n, vector<ll>(m));
    rep(i, n)
    {
        rep(j, m)
        {
            cin >> A[i][j];
        }
    }
    ll ans = 0;
    // 曲の組み合わせを全探索.
    rep(i, m)
    {
        rep(j, m - i - 1)
        {
            ll count = 0;
            rep(k, n)
            {
                count += max(A[k][i], A[k][j + i + 1]);
            }
            ans = max(ans, count);
        }
    }
    cout << ans << endl;
    return 0;
}

簡単な(diffが低い)問題で、制約が小さめのときは、必ずでは無いですが、全探索で済みますね

終わりに...

これは、毎週火曜日にやっていこうかなと思います!
では、お疲れ様でした!

競プロ生活まとめブログやります!!

はじめに...

はじめまして! ルクといいます!
誰なのかを一言で言うと弱々学生競プロerです...
もうすぐatcoder入緑茶色コーダーです
それで、このアカウントを作成した一番の理由ですがメモ用です
それを皆さんに共有できたらと思います!

これからやっていきたいことのメモ

  • AtCoderCodeforcesの解いた問題の解説!
  • 初心者のためのAtCoder入門講座!
    この2つを主にやっていきたいと思います
    AtCoderは主にABCを、稀にARCのA問題の考え方をまとめていきます
    初心者のためのAtCoder入門講座は適当な時間の余ったときに作ります

X(Twitter)やってる人です

競プロerの方はX(Twitter)のアカウントをフォローしていただけると嬉しいです!
ほぼフォロバ率100ですので!

最後に...

初投稿はこれだけにしておきます
お疲れ様でしたー