質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.35%
アルゴリズム

アルゴリズムとは、定められた目的を達成するために、プログラムの理論的な動作を定義するものです。

プログラミング言語

プログラミング言語はパソコン上で実行することができるソースコードを記述する為に扱う言語の総称です。

ループ

ループとは、プログラミングにおいて、条件に合致している間、複数回繰り返し実行される箇所や、その制御構造を指します

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

Q&A

解決済

1回答

761閲覧

動的計画法の漸化式をループさせるときに、ある添え字が別の添え字の取りうる範囲に制限を与えることに着目して、ループさせたら誤った答えが得られた。

hanyansakura

総合スコア6

アルゴリズム

アルゴリズムとは、定められた目的を達成するために、プログラムの理論的な動作を定義するものです。

プログラミング言語

プログラミング言語はパソコン上で実行することができるソースコードを記述する為に扱う言語の総称です。

ループ

ループとは、プログラミングにおいて、条件に合致している間、複数回繰り返し実行される箇所や、その制御構造を指します

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

0グッド

0クリップ

投稿2020/04/08 14:52

編集2020/04/08 15:48

前提・実現したいこと

問題文は以下の通り。

「高橋君は、N枚のカードを持っています。 i(1≤i≤N) 番目のカードには、整数 xi が書かれています。 高橋君は、これらのカードの中から 1 枚以上を選び、 選んだカードに書かれた整数の平均をちょうど A にしたいと考えています。 そのようなカードの選び方が何通りあるか求めてください。」

動的計画法を用いて漸化式を立てると次の通り。

if (j >= x[i]&&k > 0) {dp[i + 1][j][k] = dp[i][j - x[i]][k - 1] + dp[i][j][k];}
else {dp[i + 1][j][k] = dp[i][j][k];}

i:ここまでで選ぶかどうか判断し終えたカードの番号
j:選んだカードたちの値の合計
k:実際に選んだカードの枚数

これを全通りに対し実行し、最後にdp[N][kA][k]をすべてのkについて合計すれば答えが得られる。(Aは平均値であるから。j=kAとすれば良い。)

発生している問題・エラーメッセージ

質問は二点。

まず一点目。私は、

for (int i = 0; i < N; i++) {
for (int k = 0; k <= N; k++) {
for (int j = 0; j <= sum; j++) {}}}

と素直にすればいいものの、

for (int i = 0; i < N; i++) {
for (int k = 0; k <= i; k++) {
for (int j = 0; j <= sum; j++) {}}}

とした。なぜなら、ここまでで選ぶかどうか考慮し終えた番号iを、実際に選んだ枚数kが超えることなどありえないから、当然i<kのときの場合の数は0通りになるので考慮する必要がないと考えたからだ。すると、誤った答えが得られた。これはなぜだろうか。

質問二点目。
次に、質問一点目の内容を正しいソースコードに書き換えたが、最後にdp[N][kA][k]をすべてのkについて合計するところで、1≤k≤Nで合計するのではなく、1≤k≤n(但しint n=sum/a,sumは全xiの合計値)で合計した。なぜなら、選んだカードの合計値jが、nAを超えて、かつkが整数となることは数学的にあり得ないので、n<k≤Nなるkを考慮する必要はないと考えたからである。
すると、またしても誤った答えが得られた。なぜだろうか。

入門者なりに散々考えた末の質問です。どうか核心を突いた明快なご指摘を頂きたいです。よろしくお願い致します。

該当の(誤った)ソースコード

C++

1#include <iostream> 2using namespace std; 3 4int main() { 5 int N, A; 6 cin >> N >> A; 7 int x[53]; 8 int sum = 0; 9 for (int i = 0; i < N; i++) { 10 cin >> x[i]; 11 sum += x[i]; 12 } 13 long long dp[53][2550][53] = { 0 }; 14 dp[0][0][0] = 1; 15 for (int i = 0; i < N; i++) { 16 for (int k = 0; k <= i; k++) { 17 for (int j = 0; j <= sum; j++) { 18 if (j >= x[i]&&j>0) { 19 dp[i + 1][j][k] = dp[i][j - x[i]][k - 1] + dp[i][j][k]; 20 } 21 else { 22 dp[i + 1][j][k] = dp[i][j][k]; 23 } 24 } 25 } 26 } 27 long long combi; 28 combi = 0; 29 int n = 0; 30 n = sum / A; 31 for (int i = 1; i <= n; i++) { 32 combi += dp[N][i*A][i]; 33 } 34 cout << combi; 35 return 0; 36}

正しいソースコード

#include <iostream>
using namespace std;

int main() {
int N, A;
cin >> N >> A;
int x[53];
int sum = 0;
for (int i = 0; i < N; i++) {
cin >> x[i];
sum += x[i];
}
long long dp[53][2550][53] = { 0 };
dp[0][0][0] = 1;
for (int i = 0; i < N; i++) {
for (int k = 0; k <= N; k++) {
for (int j = 0; j <= sum; j++) {
if (j >= x[i]&&k > 0) {
dp[i + 1][j][k] = dp[i][j - x[i]][k - 1] + dp[i][j][k];
}
else {
dp[i + 1][j][k] = dp[i][j][k];
}
}
}
}
long long combi;
combi = 0;
int n = 0;
n = sum / A;
for (int i = 1; i <= N; i++) {
combi += dp[N][i*A][i];
}
cout << combi;
return 0;
}

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

質問1への回答

例えばx[0]=3の場合、1枚目(i=0)の検証で「取らずに合計値0」か、「取って合計値3」のパターンがありえるので、
dp[1][0][0]=1,dp[1][1][3]=1となり、それ以外のdp[1][j][k]は0のままになります。
ところが、質問者のロジックではi=0のループではk=0しか取れないため、forの中でelseにしか入れません。
そのため、dp[1][j][k]はdp[0][j][k]をそのままコピー、つまりdp[1][0][0]=1以外が0になってしまいます。
この間違いの原因は

ここまでで選ぶかどうか考慮し終えた番号iを、実際に選んだ枚数kが超えることなどありえない

と考えたことにあります。iは正しくは「今現在考慮している番号」で、kは「そこまでで実際に取ろうとしているカード枚数」になります。iが0始まりである関係上、kの最大値はi+1になります。上記理由のため、「そこまでのカードすべて取る」などが実現不可能になっています。

質問2への回答

Aがカードの平均値以上であれば問題ありませんが、カード平均値未満である場合、sum/Aがカード枚数Nを超える可能性があります。その場合、範囲外アクセスをしてしまう可能性があります。

そもそも

いろいろ、非常に効率が悪く、読みにくいコードであるように感じます。
今見ているところから次を見る形にすると、質問者さんの質問1の思考がそのまま適用しやすいですし、余計な分岐が要らなくなります。

c++

1 int subsum = 0; //その時点での合計最大値 2 for (int i = 0; i < N; i++) { 3 for (int k = 0; k <= i; k++) { //今見ているのがi番目、それまで取ったカードの最大枚数はi 4 for (int j = 0; j <= subsum; j++) { //合計値はそこまでの最大値までを見ればいい 5 dp[i + 1][j + x[i]][k + 1] += dp[i][j][k]; //x[i]を取った場合 6 dp[i + 1][j][k] += dp[i][j][k]; //x[i]を取らなかった場合 7 } 8 } 9 subsum += x[i]; 10 }

投稿2020/04/08 19:39

編集2020/04/08 20:16
swordone

総合スコア20669

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

hanyansakura

2020/04/09 02:52

明快かつ完璧なご指摘ありがとうございました。完全にすべての疑問が解決しました。 特に、提示していただいたより読みやすいコードは、入門者の自分にとって大変勉強になりました。 さらなる精進を目指します。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問