Dice Combinations
يُعطى لك عدد صحيح \(n\). تقوم برمي نرد عادل بستة أوجه (القيم من 1 إلى 6) وتجمع النتائج.
مهمتك هي حساب عدد الطرق للحصول على مجموع يساوي بالضبط \(n\) عن طريق رمي النرد مرة أو أكثر.
ترتيب الرميات مهم. على سبيل المثال، عندما يكون \(n = 3\)، فإن التسلسلات الصحيحة هي:
- \(1 + 1 + 1\)
- \(1 + 2\)
- \(2 + 1\)
- \(3\)
إذًا الجواب هو \(4\).
الإدخال:
- عدد صحيح واحد \(n\).
المخرجات:
- اطبع عدد الطرق (بتطبيق باقي القسمة على \(10^9 + 7\)).
القيود:
- \(1 \le n \le 10^6\)
مثال:
الإدخال:
3
المخرجات:
4
الحل
هذه مسألة كلاسيكية في البرمجة الديناميكية أحادية البعد.
سنستخدم إطار العمل SRTBOT لتصميم الـ DP.
فكرة الـ DP (حساب متسلسل)
سنعرّف مصفوفة dp بحيث تمثّل dp[x] عدد الطرق المختلفة للحصول على مجموع يساوي x باستخدام رميات النرد (بترتيب مهم).
سنملأ قيم dp[x] تصاعديًا من 0 حتى n بناءً على العلاقة العودية.
برهان SRTBOT مختصر
S — الحالةState
نستخدم مصفوفة نسميها dp. لكل عدد صحيح x بين 0 و n:
dp[x]هي عدد الطرق للحصول على مجموع يساويxباستخدام رميات النرد (مع اعتبار ترتيب الرميات).
في النهاية نريد قيمة dp[n].
R — العلاقة العوديةRecurrence
للوصول إلى مجموع x، يمكن أن تكون آخر رمية نرد هي:
- 1 → كان المجموع السابق
x - 1 - 2 → كان المجموع السابق
x - 2 - …
- 6 → كان المجموع السابق
x - 6
كل طريقة لصناعة المجموع x تتكوّن من:
طريقة لصناعة
x - kثم رمية نرد تساويkلبعضk ∈ {1,2,3,4,5,6}(مع شرط أنx - k ≥ 0).
لذلك العلاقة العودية هي:
\[ dp[x] = \sum_{k=1}^{6} dp[x-k], \]
مع تجاهل أي حد يكون فيه x - k < 0. كل القيم تُحسب بتطبيق باقي القسمة على \(10^9 + 7\).
T — الانتقالاتTransitions
من كل حالة x ننظر إلى الحالات السابقة:
- الحواف في مخطط الـ DP هي:
x ← x-1, x-2, ..., x-6(طالما أن القيم لا تصبح سالبة).
عمليًا في الكود، لكل x نجمع قيم dp[x-k] لجميع k من 1 إلى 6 بحيث x - k ≥ 0.
B — الحالات الابتدائيةBase cases
يوجد بالضبط تسلسل واحد يعطي مجموعًا يساوي صفرًا: التسلسل الفارغ (لا نرمي النرد أبدًا). لذا:
dp[0] = 1.
لأي
x < 0نعاملdp[x]كأنه صفر (لا يوجد أي تسلسل يعطي مجموعًا سالبًا).
جميع قيم dp[x] الأخرى (من 1 حتى n) نحسبها بالعلاقة العودية.
O — ترتيب الحسابOrder of computation
بما أن dp[x] تعتمد فقط على قيم أصغر (dp[x-1] حتى dp[x-6])، فإن الترتيب الطبيعي هو:
نبدأ بـ
dp[0].ثم نحسب:
dp[1], dp[2], ..., dp[n]بهذا الترتيب التصاعدي.
dp[0] = 1;
for (int x = 1; x <= n; x++) {
dp[x] = 0;
for (int k = 1; k <= 6; k++) {
if (x - k >= 0) {
dp[x] = (dp[x] + dp[x - k]) % MOD;
}
}
}T — التعقيد الزمني والفراغيTime and Space Complexity
لكل
xمن 1 إلىn، ننظر إلى 6 قيم سابقة كحد أقصى.إذن الزمن الكلي:
- \(O(6n) = O(n)\).
نستخدم مصفوفة
dpبحجمn+1:- الذاكرة \(O(n)\).
وهذا مناسب لقيم \(n \le 10^6\).
الكود النهائي
#include <iostream>
#include <vector>
using namespace std;
const int MOD = 1'000'000'007;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> dp(n + 1, 0);
dp[0] = 1; // one way to make sum 0: empty sequence
for (int x = 1; x <= n; x++) {
long long ways = 0;
for (int k = 1; k <= 6; k++) {
if (x - k >= 0) {
ways += dp[x - k];
if (ways >= MOD) ways -= MOD; // avoid overflow
}
}
dp[x] = (int)ways;
}
cout << dp[n] << '\n';
return 0;
}- يجب دائمًا أخذ النتيجة بتطبيق باقي القسمة على \(10^9+7\).
- استخدم
long longأثناء الجمع لتجنّب تجاوز السعة، ثم حوّل النتيجة إلىint. - يمكن أن تكون مصفوفة
dpمن نوعvector<int>لأن القيم دائمًا أصغر منMOD.