Skip to content

Add a new DP problem (N-Palindromes / CS Academy) #21

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Dynamic Programming/Hard/N-Palindromes/LinkToProblem.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[N-Palindromes - CS Academy](https://csacademy.com/ieeextreme-practice/task/n-palindromes/)
89 changes: 89 additions & 0 deletions Dynamic Programming/Hard/N-Palindromes/Solution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
### Problem: [N-Palindromes](https://csacademy.com/ieeextreme-practice/task/n-palindromes/)
### Topic: Dynamic Programming

We are given a string of length L and a number N ( 0 <= N <= L <= 500 ). We want to
compute the number of palindromes that can be produced by changing exactly N characters from
the given string. All the letters are lowercase english characters. Since the output may be
too big, we will compute it modulo 10^9 + 7.

#### Finding a good state for our DP

We have to, somehow, define a subproblem that will allow us to easily associate it with smaller subproblems. In string problems it is common to use string prefixes, suffixes, or substrings from a point i to a point j. Since we are working with palindromes, it might be convenient to work with substrings from i to j (_that are also centered around the center of the original string_).

Let's use the state `dp[i][j][k]` which will denote the number of palindromes we can create by changing exactly k characters of the substring s[i..j] (inclusive). The answer to our problem will be `dp[0][L-1][N]`.

One base case will be the strings of length 1 `(i==j)`. In
that case all strings of length 1 are palindromes, therefore
we can change the character to any one of the other 25 in the alphabet, or leave it as it is:

```
dp(i,j,k):
if ( i == j ):
if ( k == 0 ) return 1;
else if ( k == 2 ) return 25;
```

The above can only happen if L is odd `( L % 2 == 1 )`. If the length of the original string is even `( L % 2 == 0 )`, the smallest subproblem will be the strings of length 2. If s[i]==s[j] we can change both characters to any one from the 25 others and if s[i]!=s[j] we have to change either s[i] or s[j]:

```
dp(i,j,k):
if ( i == j-1 ):
if ( s[i] == s[j] ):
if ( k == 0 ) return 1;
if ( k == 1 ) return 0;
if ( k == 2 ) return 25;
else:
if ( k == 0 ) return 0;
if ( k == 1 ) return 2;
if ( k == 2 ) return 24;
```

Let's try to solve the general case `dp[i][j][k]` using smaller subproblems.

There are two cases:
* `s[i] == s[j]`: In this case we can either change nothing or __change both__ characters to
any character from the remaining 25. The palindromes obtained by changing no characters at this
step is the answer to the subproblem `dp(i+1,j-1,k)`, while the answer to the other case will
be `25 * dp(i+1,j-1,k-2)`.

* `s[i] != s[j]`: In this case, we have to make s[i] the same with s[j] before proceeding to
smaller substrings. We can either change s[i] to be s[j] or change s[j] to be s[i]. The answer
will be `2 * dp(i+1,j-1,k-1)`. If we have more changes available `k >= 2`, we can also change both
characters to any of the 24 others `24 * dp(i+1,j-1,k-2)`.

Summarizing, for the general case:

```
dp(i,j,k):
if ( s[i] == s[j] ):
return 25 * dp(i+1,j-1,k-2) + dp(i+1,j-1,k);

else:
return 2 * dp(i+1,j-1,k-1) + 24 * dp(i+1,j-1,k-2);
```

#### State Space Reduction

We can observe that we are not visiting all substrings s[i..j] but only a small subset of them,
those that are centered around our original string's center! From a substring `(i,j)` we will only
go to a substring `(i+1,j-1)` and then to `(i+2,j-2)` etc. Notice that for every point (x,y) we visit, it holds that `x+y` is constant (and in fact `x+y = N-1`) and we can uniquely identify both coordinates by knowing one of them.
As a result, only one of them is needed in the state!

If you try to think intuitively about it, imagine that the pairs (x,y) of the states we visit,
when connected, form a geometric shape in R^2. In our case, this shape is a line (`x+y=c`)
which is 1d!

All in all, we can reduce our state to `(i,k)`, `(j,k)`, or generally `(d,k)`, with `d` being something
that encapsulates the information of `i` and `j`. We have noticed that the 2 indices are equally far from the
center of our original string. A good choice for `d` could be the distance from the center:

```
if ( L % 2 == 0 ):
// d goes from 0 to L/2-1
i = L/2-d-1;
j = L/2+d;
else:
// d goes from
i = L/2-d;
j = L/2+d;
```
73 changes: 73 additions & 0 deletions Dynamic Programming/Hard/N-Palindromes/source.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#include <iostream>
#include <string>
#include <cstring>

#define MAX_N 505
#define P 1000000007
#define ll long long

using namespace std;

int T,N,L;
string s;
int memo[MAX_N][MAX_N];

ll dp(int d, int n){
if( n < 0 ) return 0;
if(L%2==0 && d==0){
if(s[L/2-1]==s[L/2]){
if(n==0) return 1;
if(n==1) return 0;
if(n==2) return 25;
else return 0;
}
else{
if(n==0) return 0;
if(n==1) return 2;
if(n==2) return 24;
else return 0;
}
}

if(L%2!=0 && d==0){
if(n==0) return 1;
if(n==1) return 25;
else return 0;
}

if(memo[d][n]!=-1) return memo[d][n];

int i,j,ans;
if(L%2 == 0){
// d goes from 0 to L/2-1
i = L/2-d-1;
j = L/2+d;

}
else{
// d goes from 0 to L/2
i = L/2-d;
j = L/2+d;
}

if(s[i]==s[j]) ans = ( (25*dp(d-1,n-2))%P + dp(d-1,n) )%P;
else ans = ((2*dp(d-1,n-1))%P + (24*dp(d-1,n-2))%P)%P;

memo[d][n] = ans;

return ans;
}

int main() {
cin>>T;
while(T--){
cin>>N>>s;
memset(memo,-1,sizeof(int)*MAX_N*MAX_N);

L = s.size();

if(L%2==0) cout<<dp(L/2-1,N)<<endl;
else cout<<dp(L/2,N)<<endl;
}

}