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
Solution
هذه مسألة كلاسيكية في البرمجة الديناميكية أحادية البعد.
سنستخدم إطار العمل 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; // small optimization
}
}
dp[x] = (int)ways;
}
cout << dp[n] << '\n';
return 0;
}- يجب دائمًا أخذ النتيجة بتطبيق باقي القسمة على \(10^9+7\).
- استخدم
long longأثناء الجمع لتجنّب تجاوز السعة، ثم حوّل النتيجة إلىint. - يمكن أن تكون مصفوفة
dpمن نوعvector<int>لأن القيم دائمًا أصغر منMOD.