Before attempting this problem, you should be comfortable with:
Recursion - Breaking the problem into smaller subproblems by considering each die roll
Dynamic Programming - Using memoization or tabulation to avoid redundant calculations
Modular arithmetic - Applying modulo operations to prevent integer overflow
State representation - Defining DP states based on number of dice and remaining target sum
1. Recursion
Intuition
This is a counting problem where we need to find all ways to roll n dice (each with k faces) to get a sum of target. For each die, we can roll any value from 1 to k, and we need to count how many combinations lead to the target. This naturally leads to a recursive approach: for each die roll, we try all possible face values and recursively count the ways to achieve the remaining target with the remaining dice.
Algorithm
Define a recursive function count(n, target) that returns the number of ways to reach target using n dice.
Base case: If n == 0, return 1 if target == 0 (found a valid combination), else return 0.
If target < 0, return 0 (invalid path).
For each face value from 1 to k, recursively count ways with n - 1 dice and target - val.
Sum up all the ways and return the result modulo 10^9 + 7.
classSolution:defnumRollsToTarget(self, n:int, k:int, target:int)->int:
MOD =10**9+7defcount(n, target):if n ==0:return1if target ==0else0if target <0:return0
res =0for val inrange(1, k +1):
res =(res + count(n -1, target - val))% MOD
return res
return count(n, target)
publicclassSolution{privatestaticfinalintMOD=1_000_000_007;publicintnumRollsToTarget(int n,int k,int target){returncount(n, target, k);}privateintcount(int n,int target,int k){if(n ==0){return target ==0?1:0;}if(target <0){return0;}int res =0;for(int val =1; val <= k; val++){
res =(res +count(n -1, target - val, k))%MOD;}return res;}}
classSolution{private:constint MOD =1e9+7;public:intnumRollsToTarget(int n,int k,int target){returncount(n, target, k);}private:intcount(int n,int target,int k){if(n ==0){return target ==0?1:0;}if(target <0){return0;}int res =0;for(int val =1; val <= k; val++){
res =(res +count(n -1, target - val, k))% MOD;}return res;}};
classSolution{/**
* @param {number} n
* @param {number} k
* @param {number} target
* @return {number}
*/numRollsToTarget(n, k, target){constMOD=1e9+7;constcount=(n, target)=>{if(n ===0){return target ===0?1:0;}if(target <0){return0;}let res =0;for(let val =1; val <= k; val++){
res =(res +count(n -1, target - val))%MOD;}return res;};returncount(n, target);}}
publicclassSolution{privateconstint MOD =1000000007;publicintNumRollsToTarget(int n,int k,int target){returnCount(n, target, k);}privateintCount(int n,int target,int k){if(n ==0){return target ==0?1:0;}if(target <0){return0;}int res =0;for(int val =1; val <= k; val++){
res =(res +Count(n -1, target - val, k))% MOD;}return res;}}
funcnumRollsToTarget(n int, k int, target int)int{
MOD :=int(1e9+7)var count func(n, target int)int
count =func(n, target int)int{if n ==0{if target ==0{return1}return0}if target <0{return0}
res :=0for val :=1; val <= k; val++{
res =(res +count(n-1, target-val))% MOD
}return res
}returncount(n, target)}
class Solution {privateval MOD =1_000_000_007funnumRollsToTarget(n: Int, k: Int, target: Int): Int {returncount(n, target, k)}privatefuncount(n: Int, target: Int, k: Int): Int {if(n ==0){returnif(target ==0)1else0}if(target <0){return0}var res =0for(v in1..k){
res =(res +count(n -1, target - v, k))% MOD
}return res
}}
classSolution{letMOD=1_000_000_007funcnumRollsToTarget(_ n:Int,_ k:Int,_ target:Int)->Int{returncount(n, target, k)}privatefunccount(_ n:Int,_ target:Int,_ k:Int)->Int{if n ==0{return target ==0?1:0}if target <0{return0}var res =0for val in1...k {
res =(res +count(n -1, target - val, k))%MOD}return res
}}
Time & Space Complexity
Time complexity: O(kn)
Space complexity: O(n)
Where n is the number of dices, k is the number of faces each dice have, and t is the target value.
2. Dynamic Programming (Top-Down)
Intuition
The recursive solution has overlapping subproblems. For example, reaching target 10 with 3 dice might be computed multiple times through different paths. By caching results in a memoization table, we avoid redundant calculations. The state is defined by two variables: the number of dice remaining and the current target sum.
Algorithm
Create a cache (dictionary or 2D array) to store computed results.
Define a recursive function count(n, target) with memoization.
Base case: If n == 0, return 1 if target == 0, else return 0.
If target < 0, return 0.
If the result for (n, target) is already cached, return it.
For each face value from 1 to k (where target - val >= 0), add the recursive result.
classSolution:defnumRollsToTarget(self, n:int, k:int, target:int)->int:
MOD =10**9+7
cache ={}defcount(n, target):if n ==0:return1if target ==0else0if(n, target)in cache:return cache[(n, target)]
res =0for val inrange(1, k +1):if target - val >=0:
res =(res + count(n -1, target - val))% MOD
cache[(n, target)]= res
return res
return count(n, target)
publicclassSolution{privatestaticfinalintMOD=1_000_000_007;privateint[][] dp;publicintnumRollsToTarget(int n,int k,int target){
dp =newint[n +1][target +1];for(int[] row : dp){Arrays.fill(row,-1);}returncount(n, target, k);}privateintcount(int n,int target,int k){if(n ==0){return target ==0?1:0;}if(target <0){return0;}if(dp[n][target]!=-1){return dp[n][target];}int res =0;for(int val =1; val <= k; val++){if(target - val >=0){
res =(res +count(n -1, target - val, k))%MOD;}}
dp[n][target]= res;return res;}}
classSolution{private:constint MOD =1e9+7;
vector<vector<int>> dp;public:intnumRollsToTarget(int n,int k,int target){
dp =vector<vector<int>>(n +1,vector<int>(target +1,-1));returncount(n, target, k);}private:intcount(int n,int target,int k){if(n ==0){return target ==0?1:0;}if(target <0){return0;}if(dp[n][target]!=-1){return dp[n][target];}int res =0;for(int val =1; val <= k; val++){if(target - val >=0){
res =(res +count(n -1, target - val, k))% MOD;}}
dp[n][target]= res;return res;}};
classSolution{/**
* @param {number} n
* @param {number} k
* @param {number} target
* @return {number}
*/numRollsToTarget(n, k, target){constMOD=1e9+7;const dp = Array.from({length: n +1},()=>Array(target +1).fill(-1),);constcount=(n, target)=>{if(n ===0){return target ===0?1:0;}if(dp[n][target]!==-1){return dp[n][target];}let res =0;for(let val =1; val <= k; val++){if(target - val >=0){
res =(res +count(n -1, target - val))%MOD;}}
dp[n][target]= res;return res;};returncount(n, target);}}
publicclassSolution{privateconstint MOD =1000000007;privateint[,] dp;publicintNumRollsToTarget(int n,int k,int target){
dp =newint[n +1, target +1];for(int i =0; i <= n; i++){for(int j =0; j <= target; j++){
dp[i, j]=-1;}}returnCount(n, target, k);}privateintCount(int n,int target,int k){if(n ==0){return target ==0?1:0;}if(target <0){return0;}if(dp[n, target]!=-1){return dp[n, target];}int res =0;for(int val =1; val <= k; val++){if(target - val >=0){
res =(res +Count(n -1, target - val, k))% MOD;}}
dp[n, target]= res;return res;}}
funcnumRollsToTarget(n int, k int, target int)int{
MOD :=int(1e9+7)
dp :=make([][]int, n+1)for i :=range dp {
dp[i]=make([]int, target+1)for j :=range dp[i]{
dp[i][j]=-1}}var count func(n, target int)int
count =func(n, target int)int{if n ==0{if target ==0{return1}return0}if target <0{return0}if dp[n][target]!=-1{return dp[n][target]}
res :=0for val :=1; val <= k; val++{if target-val >=0{
res =(res +count(n-1, target-val))% MOD
}}
dp[n][target]= res
return res
}returncount(n, target)}
class Solution {privateval MOD =1_000_000_007privatelateinitvar dp: Array<IntArray>funnumRollsToTarget(n: Int, k: Int, target: Int): Int {
dp =Array(n +1){IntArray(target +1){-1}}returncount(n, target, k)}privatefuncount(n: Int, target: Int, k: Int): Int {if(n ==0){returnif(target ==0)1else0}if(target <0){return0}if(dp[n][target]!=-1){return dp[n][target]}var res =0for(v in1..k){if(target - v >=0){
res =(res +count(n -1, target - v, k))% MOD
}}
dp[n][target]= res
return res
}}
classSolution{letMOD=1_000_000_007var dp:[[Int]]=[]funcnumRollsToTarget(_ n:Int,_ k:Int,_ target:Int)->Int{
dp =Array(repeating:Array(repeating:-1, count: target +1), count: n +1)returncount(n, target, k)}privatefunccount(_ n:Int,_ target:Int,_ k:Int)->Int{if n ==0{return target ==0?1:0}if target <0{return0}if dp[n][target]!=-1{return dp[n][target]}var res =0for val in1...k {if target - val >=0{
res =(res +count(n -1, target - val, k))%MOD}}
dp[n][target]= res
return res
}}
Time & Space Complexity
Time complexity: O(n∗t∗k)
Space complexity: O(n∗t)
Where n is the number of dices, k is the number of faces each dice have, and t is the target value.
3. Dynamic Programming (Bottom-Up)
Intuition
Instead of recursion with memoization, we can build the solution iteratively from the ground up. We use a 2D DP table where dp[i][t] represents the number of ways to achieve sum t using exactly i dice. We fill this table by considering each die one at a time and each possible face value.
Algorithm
Create a 2D array dp of size (n + 1) x (target + 1), initialized to 0.
Set dp[0][0] = 1 (one way to achieve sum 0 with 0 dice).
classSolution:defnumRollsToTarget(self, n:int, k:int, target:int)->int:
MOD =10**9+7
dp =[[0]*(target +1)for _ inrange(n +1)]
dp[0][0]=1for i inrange(1, n +1):for val inrange(1, k +1):for t inrange(val, target +1):
dp[i][t]=(dp[i][t]+ dp[i -1][t - val])% MOD
return dp[n][target]
publicclassSolution{publicintnumRollsToTarget(int n,int k,int target){intMOD=1_000_000_007;int[][] dp =newint[n +1][target +1];
dp[0][0]=1;for(int i =1; i <= n; i++){for(int val =1; val <= k; val++){for(int t = val; t <= target; t++){
dp[i][t]=(dp[i][t]+ dp[i -1][t - val])%MOD;}}}return dp[n][target];}}
classSolution{public:intnumRollsToTarget(int n,int k,int target){constint MOD =1e9+7;
vector<vector<int>>dp(n +1,vector<int>(target +1,0));
dp[0][0]=1;for(int i =1; i <= n; i++){for(int val =1; val <= k; val++){for(int t = val; t <= target; t++){
dp[i][t]=(dp[i][t]+ dp[i -1][t - val])% MOD;}}}return dp[n][target];}};
classSolution{/**
* @param {number} n
* @param {number} k
* @param {number} target
* @return {number}
*/numRollsToTarget(n, k, target){constMOD=1e9+7;const dp = Array.from({length: n +1},()=>Array(target +1).fill(0),);
dp[0][0]=1;for(let i =1; i <= n; i++){for(let val =1; val <= k; val++){for(let t = val; t <= target; t++){
dp[i][t]=(dp[i][t]+ dp[i -1][t - val])%MOD;}}}return dp[n][target];}}
publicclassSolution{publicintNumRollsToTarget(int n,int k,int target){int MOD =1_000_000_007;int[,] dp =newint[n +1, target +1];
dp[0,0]=1;for(int i =1; i <= n; i++){for(int val =1; val <= k; val++){for(int t = val; t <= target; t++){
dp[i, t]=(dp[i, t]+ dp[i -1, t - val])% MOD;}}}return dp[n, target];}}
funcnumRollsToTarget(n int, k int, target int)int{
MOD :=int(1e9+7)
dp :=make([][]int, n+1)for i :=range dp {
dp[i]=make([]int, target+1)}
dp[0][0]=1for i :=1; i <= n; i++{for val :=1; val <= k; val++{for t := val; t <= target; t++{
dp[i][t]=(dp[i][t]+ dp[i-1][t-val])% MOD
}}}return dp[n][target]}
class Solution {funnumRollsToTarget(n: Int, k: Int, target: Int): Int {val MOD =1_000_000_007val dp =Array(n +1){IntArray(target +1)}
dp[0][0]=1for(i in1..n){for(v in1..k){for(t in v..target){
dp[i][t]=(dp[i][t]+ dp[i -1][t - v])% MOD
}}}return dp[n][target]}}
classSolution{funcnumRollsToTarget(_ n:Int,_ k:Int,_ target:Int)->Int{letMOD=1_000_000_007var dp =Array(repeating:Array(repeating:0, count: target +1), count: n +1)
dp[0][0]=1for i in1...n {for val in1...k {for t in val...target {
dp[i][t]=(dp[i][t]+ dp[i -1][t - val])%MOD}}}return dp[n][target]}}
Time & Space Complexity
Time complexity: O(n∗t∗k)
Space complexity: O(n∗t)
Where n is the number of dices, k is the number of faces each dice have, and t is the target value.
4. Dynamic Programming (Space Optimized)
Intuition
In the bottom-up approach, we only need the previous row to compute the current row. This observation allows us to reduce space from O(n * target) to O(target) by using two 1D arrays: one for the current state and one for the next state. After processing each die, we swap the arrays.
Algorithm
Create a 1D array dp of size target + 1, initialized to 0.
Where n is the number of dices, k is the number of faces each dice have, and t is the target value.
5. Dynamic Programming (Optimal)
Intuition
We can use a single array and iterate backwards to avoid overwriting values we still need. By processing sums in reverse order and resetting values before adding new contributions, we achieve the same result with a single array. This approach also skips positions with zero ways, providing a minor optimization.
Algorithm
Create a 1D array dp of size target + 1, initialized to 0.
Set dp[0] = 1.
For each die (from 0 to n - 1):
Iterate t from target down to 0:
Store the current value ways = dp[t] and reset dp[t] = 0.
If ways > 0, for each face value val from 1 to min(k, target - t):
classSolution:defnumRollsToTarget(self, n:int, k:int, target:int)->int:
MOD =10**9+7
dp =[0]*(target +1)
dp[0]=1for dice inrange(n):for t inrange(target,-1,-1):
ways = dp[t]
dp[t]=0if ways:for val inrange(1,min(k +1, target - t +1)):
dp[t + val]=(dp[t + val]+ ways)% MOD
return dp[target]
publicclassSolution{publicintnumRollsToTarget(int n,int k,int target){intMOD=1_000_000_007;int[] dp =newint[target +1];
dp[0]=1;for(int dice =0; dice < n; dice++){for(int t = target; t >=0; t--){int ways = dp[t];
dp[t]=0;if(ways >0){for(int val =1; val <=Math.min(k, target - t); val++){
dp[t + val]=(dp[t + val]+ ways)%MOD;}}}}return dp[target];}}
classSolution{public:intnumRollsToTarget(int n,int k,int target){constint MOD =1e9+7;
vector<int>dp(target +1,0);
dp[0]=1;for(int dice =0; dice < n; dice++){for(int t = target; t >=0; t--){int ways = dp[t];
dp[t]=0;if(ways >0){for(int val =1; val <=min(k, target - t); val++){
dp[t + val]=(dp[t + val]+ ways)% MOD;}}}}return dp[target];}};
classSolution{/**
* @param {number} n
* @param {number} k
* @param {number} target
* @return {number}
*/numRollsToTarget(n, k, target){constMOD=1e9+7;const dp =Array(target +1).fill(0);
dp[0]=1;for(let dice =0; dice < n; dice++){for(let t = target; t >=0; t--){const ways = dp[t];
dp[t]=0;if(ways >0){for(let val =1; val <= Math.min(k, target - t); val++){
dp[t + val]=(dp[t + val]+ ways)%MOD;}}}}return dp[target];}}
publicclassSolution{publicintNumRollsToTarget(int n,int k,int target){int MOD =1_000_000_007;int[] dp =newint[target +1];
dp[0]=1;for(int dice =0; dice < n; dice++){for(int t = target; t >=0; t--){int ways = dp[t];
dp[t]=0;if(ways >0){for(int val =1; val <= Math.Min(k, target - t); val++){
dp[t + val]=(dp[t + val]+ ways)% MOD;}}}}return dp[target];}}
funcnumRollsToTarget(n int, k int, target int)int{
MOD :=int(1e9+7)
dp :=make([]int, target+1)
dp[0]=1for dice :=0; dice < n; dice++{for t := target; t >=0; t--{
ways := dp[t]
dp[t]=0if ways >0{for val :=1; val <= k && val <= target-t; val++{
dp[t+val]=(dp[t+val]+ ways)% MOD
}}}}return dp[target]}
class Solution {funnumRollsToTarget(n: Int, k: Int, target: Int): Int {val MOD =1_000_000_007val dp =IntArray(target +1)
dp[0]=1for(dice in0 until n){for(t in target downTo 0){val ways = dp[t]
dp[t]=0if(ways >0){for(v in1..minOf(k, target - t)){
dp[t + v]=(dp[t + v]+ ways)% MOD
}}}}return dp[target]}}
Where n is the number of dices, k is the number of faces each dice have, and t is the target value.
Common Pitfalls
Forgetting Modulo Operations
Not applying the modulo operation (% (10^9 + 7)) after each addition causes integer overflow. The number of ways can grow extremely large, exceeding the maximum value of 32-bit or even 64-bit integers. Apply modulo after every addition to keep values within bounds and avoid overflow errors.
Incorrect Base Case Handling
Setting the wrong base case in the DP leads to incorrect counts. When n == 0 (no dice left), the only valid result is if target == 0 (we exactly hit the target), returning 1. If target != 0 with no dice remaining, return 0. Mixing up these conditions produces wrong answers for edge cases.
Off-by-One Errors in Face Values
Starting the face value loop at 0 instead of 1, or going up to k - 1 instead of k, miscounts the valid dice rolls. Each die has faces numbered 1 through k (not 0 through k-1). Ensure your loop iterates from 1 to k inclusive to correctly enumerate all possible face values.