SPCP6 - Editorial

PROBLEM LINK:

Practice
Contest: Division 1
Contest: Division 2
Contest: Division 3
Contest: Division 4

Author: alpha_ashwin, shanmukh29
Tester: raysh07
Editorialist: iceknight1093

DIFFICULTY:

TBD

PREREQUISITES:

Dynamic programming

PROBLEM:

Chef and Chefina play tetris together. Chef starts first.
A player can clear between 1 and 4 lines on each move. If they clear only one line, the turn passes to the other player.
The game ends when at least L lines have been cleared.

Count the number of sequences of moves that end with Chef finishing the game with a “Tetris”, i.e, clearing 4 lines.

EXPLANATION:

Let f(x) denote the number of sequences such that exactly x lines have been cleared, and it’s currently Chef’s turn.
Similarly, let g(x) denote the number of sequences with x lines cleared, and it’s Chefina’s turn.

As base cases, we have f(0) = 1 and g(0) = 0, since Chef starts the game.
Also, f(x) = g(x) = 0 when x \lt 0.

Notice that functions f and g can be computed recursively:

f(x) = f(x-4) + f(x-3) + f(x-2) + g(x-1) \\ g(x) = g(x-4) + g(x-3) + g(x-2) + f(x-1) \\

by conditioning on the last move made: if \geq 2 lines were cleared the player doesn’t change; if 1 line was cleared it must’ve been done by the other player.

Now, Chef wants to be the one to the end the game, and do it by clearing 4 lines.
Since the game ends when L lines are cleared, just before Chef’s last move the number of lines cleared must’ve been \geq L-4 and it should be Chef’s turn.
So, the answer we’re looking for is just

f(L-1) + f(L-2) + f(L-3) + f(L-4)

All the f(x) and g(x) values from 0 to 10^5 can be precomputed in linear time with dynamic programming, after which each testcase can be answered in \mathcal{O}(1) time.

TIME COMPLEXITY:

\mathcal{O}(1) per testcase after \mathcal{O}(\max L) precomputation.

CODE:

Author's code (C++)
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
#define int long long int
#define vi vector<int>
int mod= 1e9+7;
int32_t main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);

    int zero=0;
    vi dpchef(1e5,0), dpchefina(1e5,0);
    dpchefina[1]=1;
    dpchef[2]=1;
    dpchef[3]=1;
    dpchef[4]=1;
    for(int i=1;i<1e5;i++)
    {
        dpchef[i]+=(dpchefina[max(zero,i-1)] + dpchef[max(zero,i-2)] + dpchef[max(zero,i-3)] + dpchef[max(zero,i-4)])%mod;
        dpchefina[i]+=(dpchef[max(zero,i-1)] + dpchefina[max(zero,i-2)] + dpchefina[max(zero,i-3)] + dpchefina[max(zero,i-4)])%mod;
    }
    int _t=1;
    cin>>_t;
    for(int test=1;test<=_t;test++)
    {
        int n;                                          
        cin>>n;
        int answer=(dpchef[max(zero,n-1)]+dpchef[max(zero,n-2)]+dpchef[max(zero,n-3)]+dpchef[max(zero,n-4)])%mod;
        if(n<=4)
            answer++;
        cout<<answer<<"\n";
    }
}
Tester's code (C++)
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define INF (int)1e18
#define f first
#define s second

mt19937_64 RNG(chrono::steady_clock::now().time_since_epoch().count());

struct input_checker {
    string buffer;
    int pos;

    const string all = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    const string number = "0123456789";
    const string upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    const string lower = "abcdefghijklmnopqrstuvwxyz";

    input_checker() {
        pos = 0;
        while (true) {
            int c = cin.get();
            if (c == -1) {
                break;
            }
            buffer.push_back((char) c);
        }
    }

    int nextDelimiter() {
        int now = pos;
        while (now < (int) buffer.size() && !isspace(buffer[now])) {
            now++;
        }
        return now;
    }

    string readOne() {
        assert(pos < (int) buffer.size());
        int nxt = nextDelimiter();
        string res;
        while (pos < nxt) {
            res += buffer[pos];
            pos++;
        }
        return res;
    }

    string readString(int minl, int maxl, const string &pattern = "") {
        assert(minl <= maxl);
        string res = readOne();
        assert(minl <= (int) res.size());
        assert((int) res.size() <= maxl);
        for (int i = 0; i < (int) res.size(); i++) {
            assert(pattern.empty() || pattern.find(res[i]) != string::npos);
        }
        return res;
    }

    int readInt(int minv, int maxv) {
        assert(minv <= maxv);
        int res = stoi(readOne());
        assert(minv <= res);
        assert(res <= maxv);
        return res;
    }

    long long readLong(long long minv, long long maxv) {
        assert(minv <= maxv);
        long long res = stoll(readOne());
        assert(minv <= res);
        assert(res <= maxv);
        return res;
    }

    auto readInts(int n, int minv, int maxv) {
        assert(n >= 0);
        vector<int> v(n);
        for (int i = 0; i < n; ++i) {
            v[i] = readInt(minv, maxv);
            if (i+1 < n) readSpace();
        }
        return v;
    }

    auto readLongs(int n, long long minv, long long maxv) {
        assert(n >= 0);
        vector<long long> v(n);
        for (int i = 0; i < n; ++i) {
            v[i] = readLong(minv, maxv);
            if (i+1 < n) readSpace();
        }
        return v;
    }

    void readSpace() {
        assert((int) buffer.size() > pos);
        assert(buffer[pos] == ' ');
        pos++;
    }

    void readEoln() {
        assert((int) buffer.size() > pos);
        assert(buffer[pos] == '\n');
        pos++;
    }

    void readEof() {
        assert((int) buffer.size() == pos);
    }
};

input_checker inp;

const int T = 1e5;
const int N = 1e5;
const int mod = 1e9 + 7;
int dp[N + 1][2];
vector <int> a = {1, 2, 3, 4};

void pre(){
    int n = N;
    for (int i = 0; i <= n; i++){
        dp[i][0] = dp[i][1] = 0;
    }

    dp[0][1] = 1;
    dp[0][0] = 1;

    for (int i = 1; i <= n; i++){
        for (int j = 0; j < 2; j++){
            for (auto x : a){
                int l = i - x;
                if (l < 0) l = 0;
                if (l == 0 && (x != 4 || j != 1)) continue;

                dp[i][j] += dp[l][j ^ (x == 1)];
                dp[i][j] %= mod;
            }
        }
    }
}

void Solve() 
{
    int n = inp.readInt(1, N); inp.readEoln();
    // int n; cin >> n;
    
    cout << dp[n][1] << "\n";
}

int32_t main() 
{
    auto begin = std::chrono::high_resolution_clock::now();
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    int t = 1;
    // freopen("in",  "r", stdin);
    // freopen("out", "w", stdout);
    
    t = inp.readInt(1, T);
    inp.readEoln();
    
    //cin >> t;
    
    pre();

    for(int i = 1; i <= t; i++) 
    {
        //cout << "Case #" << i << ": ";
        Solve();
    }

    inp.readEof();

    auto end = std::chrono::high_resolution_clock::now();
    auto elapsed = std::chrono::duration_cast<std::chrono::nanoseconds>(end - begin);
    cerr << "Time measured: " << elapsed.count() * 1e-9 << " seconds.\n"; 
    return 0;
}
Editorialist's code (Python)
mod = 10**9 + 7
N = 10**5 + 10
dp = [ [0, 0] for _ in range(N) ]
# dp[i][0] -> exactly i lines cleared, and it's now Chef's turn

dp[0][0] = 1
for i in range(1, N):
    for j in range(2, 5):
        if i-j >= 0:
            dp[i][0] += dp[i-j][0]
            dp[i][1] += dp[i-j][1]
    dp[i][0] += dp[i-1][1]
    dp[i][1] += dp[i-1][0]
    dp[i][0] %= mod
    dp[i][1] %= mod

for _ in range(int(input())):
    n = int(input())
    ans = 0
    for x in range(1, 5):
        if n-x >= 0: ans += dp[n-x][0]
    print(ans % mod)

I used the same approach of creating two different recurrences. When I submitted the code, it gave RTE, but again when I submitted it (the same exact code), it gave me AC!! Any ideas on what may have caused this behavior?

RTE - 1032597958
AC - 1032599054

Hey @roumak Could you please explain how did you get to know that values have to be memoized(?) and little bit explanation about your code cause I liked it very much, I love memoization solutions rather than DP tabulation solutions.

Sure.
Answering your first question,

In most of the dp problems where recurrence is involved, the same value is calculated many times. This makes it obvious that we need memoization. When I was learning about db, I was taught about a method where we can map out the dp algorithm using some kind of tree. I suggest you visit structy.net and do their dp problems. They have video tutorials that explain the approach as well as the coding part. (12 out of 16 dp problems present on their website are free).

Answering your query,

I believe my approach and the approach mentioned in the editorial are exactly the same. If you ask, how I get to this approach? It took me more than 30 minutes to just figure out that this could be solved with some kind of recurrence relation. The first thing that came to me was extensive PnC, but I figured out that it wasn’t gonna work. I tried finding patterns, but that failed as well. Finally, after a lot of brainstorming, I could form a recurrence relation.
For the base cases, I just manually counted them, and the rest was comparatively easy.
Hope it helps!! Happy Coding! :smile:

1 Like

Thanks for getting back to me! I’ll definitely check out structy.net as you suggested. It’s comforting to know that I’m not the only one who sometimes takes a bit longer to figure things out.I appreciate your honesty about the 30 minutes. It makes me feel more relatable. Thanks again and happy coding! :blush:

1 Like