Official

D - Money in Hand Editorial by mechanicalpenciI


この問題は動的計画法によって解く事ができます。

bool型の\(2\) 次元配列 \(dp[i][j]\) \((0\leq i\leq N, 0\leq j\leq X)\) を、「\(A_1\) 円硬貨を \(B_1\) 枚, \(A_2\) 円硬貨を \(B_2\) 枚, \(\ldots\), \(A_i\) 円硬貨を \(B_i\) 枚を用いてちょうど \(j\) 円を払えるならば True , 払えないならば False」として定め、これを \(i=0,1,\ldots,N\) の順で求める事を考えます。
問題に対する答えは、\(dp[N][X]\)True ならば YesFalse ならば No となります。

まず、\(i=0\) の場合について、高橋君は \(1\) 枚も硬貨を持っていないため、\(dp[0][0]=\) True , \(j>0\) のとき \(dp[0][j]=\) False です。

さて、\(dp[i-1][j]\) \((0\leq j\leq X)\) が求まっているとき、\(dp[i][j]\) を求める事を考えます。
\(A_i\) 円硬貨は\(0\) 枚以上 \(B_i\) 枚以下の範囲で自由に使えるので、 \(0\leq k\leq B_i\) をみたす整数 \(k\) であって、\(j-kA_i\geq 0\) かつ \(dp[i-1][j-kA_i]=\) True であるようなものが \(1\) つでも存在すれば \(dp[i][j]\)True となり、そうでないならば False となります。 すなわち、

\[ dp[i][j]=dp[i-1][j]\lor dp[i-1][j-A_i]\lor dp[i-1][j-2A_i]\lor \cdots\lor dp[i-1][j-B_iA_i]\quad (ただし、j'<0 ならば dp[i-1][j']=\text{False}) \]

となります。

これをナイーブに実装すると, 計算量は 各\(i\) について \(O(B_iX)\) となるため全体で \(O((\displaystyle\sum_{i=1}^N B_i)X)\) であり、今回の制約下で十分高速に動きます。よって、この問題を解く事ができました。

\(O(NX)\) 解法について

なお、この問題には \(O(NX)\) 解法が存在します。 基本的な方針は上記の方針と同じですが、\(dp[i][j]\) を求める前に各\(j\) について、
\(dp'[i-1][j]=\) ( \(j'=j,j-A_i,j-2A_i,\ldots\) のうち \(dp[i-1][j']=\)True であるような最大の\(j'\) )(存在しなければ\(-\infty\)) を計算します。

\(dp'[i-1][j]\)\(dp[i-1][j]=\)True ならば \(dp'[i-1][j]=j\), そうでなければ \(dp'[i-1][j]=dp'[i-1][j-A_i]\) として\(j\) が小さい方から順に\(O(1)\) で計算する事ができます。 さらに、本解法で求めた漸化式から, \(dp[i][j]\)\(dp'[i-1][j]\geq j-A_iB_i\) ならば True, そうでないならばFalse としてこれも \(O(1)\) で計算する事ができます。
よって、各\(i\) について \(O(X)\), 全体で \(O(NX)\) で解く事ができました。

c++ による実装例 (本解法):

#include <bits/stdc++.h>
using namespace std;

int main(void){
	int n,x;
	int a[50];
	int b[50];
	bool dp[51][10001];

	cin>>n>>x;
	for(int i=0;i<n;i++)cin>>a[i]>>b[i];

	for(int i=0;i<=n;i++)for(int j=0;j<=x;j++)dp[i][j]=false;
	dp[0][0]=true;
	for(int i=0;i<n;i++){
		for(int j=0;j<=x;j++){
			for(int k=0;k<=b[i];k++){
				if(j>=a[i]*k){
					if(dp[i][j-a[i]*k])dp[i+1][j]=true;
				}
			}
		}
	}

	if(dp[n][x])cout<<"Yes"<<endl;
	else cout<<"No"<<endl;
	return 0;
}

c++ による実装例(\(O(NX)\)解法) :

#include <bits/stdc++.h>
using namespace std;

int main(void){
	int n,x;
	int a[50];
	int b[50];
	bool dp[10001]; //配列は使い回し
	int mx[100];

	cin>>n>>x;
	for(int i=0;i<n;i++)cin>>a[i]>>b[i];

	for(int i=0;i<=x;i++)dp[i]=false;
	dp[0]=true;
	for(int i=0;i<n;i++){
		for(int j=0;j<a[i];j++)mx[j]=-10000; //-a[i]*b[i]未満なら良い
		for(int j=0;j<=x;j++){
			if(dp[j])mx[j%a[i]]=j;
			if(mx[j%a[i]]>=j-(a[i]*b[i]))dp[j]=true;
			else dp[j]=false;
		}
	}

	if(dp[x])cout<<"Yes"<<endl;
	else cout<<"No"<<endl;
	return 0;
}

posted:
last update: