Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Dynamic Programming Solutions for Three Classic Problem Variants

Tech Oct 18 4

This problem explores a classic dynamic programming approach with interesting variations. The fundamental question involves counting subsequences with certain properties.

When the task is to count substrings, we use a well-established DP formulation:

Define fi,j as the number of valid ways considering the first i characters, where there are j unmatched opening parentheses. The transitions are:

  • If si is '(' or '?': fi,j += fi-1,j-1
  • If si is ')' or '?': fi,j += fi-1,j+1

For subsequence counting, we introduce an additional transition: fi,j += fi-1,j

Here's the implemantation using prefix sum optimization:

const int MAXN = 5005;
const int MOD = 1000000007;

int dp[MAXN][MAXN];
int prefix[MAXN][MAXN];
char str[MAXN];

int main() {
    int len;
    scanf("%s", str + 1);
    len = strlen(str + 1);
    
    prefix[0][0] = dp[0][0] = 1;
    prefix[1][0] = dp[1][0] = 1;
    
    for (int i = 1; i <= len; i++) {
        for (int j = 0; j <= len; j++) {
            dp[i][j] = dp[i-1][j];
            
            if (j > 0 && (str[i] == '(' || str[i] == '?')) {
                dp[i][j] = (prefix[i-1][j-1] + dp[i][j]) % MOD;
            }
            if (str[i] == ')' || str[i] == '?') {
                dp[i][j] = (prefix[i-1][j+1] + dp[i][j]) % MOD;
            }
            prefix[i][j] = dp[i][j];
        }
    }
    
    printf("%d\n", prefix[len][0]);
    return 0;
}

Problem 2: Optimal Array Rearrangement

This problem involves minimizing the sum of all prefix sums after rearranging elements from multiple arrays into one. When m (the number of operations) is very large, upgrading all element yields maximum benefit.

The key insight is to work backwards: if element a[j] is upgraded at minute j, we subtract the benefit of not having it for those j minutes. This transforms the problem into: given n arrays ai, arrange all elements into a new array b of length Σki such that elements from each original array maintain their relative order, and the sum of prefix sums of b is minimized.

Without order constraints, sorting all elements ascending minimizes the sum of prefix sums. A key lemma states: in array b, placing one segment before another is valid if and only if the average of the first segment is smaller than the average of the second.

Therefore, our strategy is: at each step, select the segment with the smallest average from all arrays and append it to b. Each array should be pre-segmented so that averages within each array are monotonically non-decreasing.

For optimal segmentation, we process each array as follows: when adding an element increases the current segment's average, we start a new segment. Otherwise, we merge with the previous segment. Using a stack-based union-find approach ensures O(n) complexity per array, as each merge reduces the number of segments.

#include <bits/stdc++.h>
using namespace std;

struct Segment {
    long long sum;
    int cnt;
    double avg;
    int arrIdx;
    int startPos;
    int endPos;
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    int n;
    cin >> n;
    vector<vector<int>> arr(n);
    
    for (int i = 0; i < n; i++) {
        int k;
        cin >> k;
        arr[i].resize(k);
        for (int j = 0; j < k; j++) {
            cin >> arr[i][j];
        }
    }
    
    vector<Segment> allSegments;
    
    for (int i = 0; i < n; i++) {
        vector<Segment> localSeg;
        long long runningSum = 0;
        int count = 0;
        
        for (int val : arr[i]) {
            runningSum += val;
            count++;
            Segment newSeg = {runningSum, count, 
                           (double)runningSum / count, i, 
                           (int)localSeg.size(), (int)localSeg.size()};
            
            while (!localSeg.empty()) {
                Segment last = localSeg.back();
                double oldAvg = (double)last.sum / last.cnt;
                double newAvg = newSeg.sum / newSeg.cnt;
                
                if (oldAvg > newAvg) {
                    localSeg.pop_back();
                    newSeg.sum = last.sum + newSeg.sum;
                    newSeg.cnt = last.cnt + newSeg.cnt;
                    newSeg.avg = (double)newSeg.sum / newSeg.cnt;
                    newSeg.startPos = last.startPos;
                } else {
                    break;
                }
            }
            localSeg.push_back(newSeg);
        }
        
        for (auto &seg : localSeg) {
            allSegments.push_back(seg);
        }
    }
    
    sort(allSegments.begin(), allSegments.end(), 
         [](const Segment& a, const Segment& b) {
             return a.avg < b.avg;
         });
    
    long long result = 0;
    long long prefixSum = 0;
    
    for (const auto& seg : allSegments) {
        long long segmentSum = seg.sum;
        result += prefixSum;
        prefixSum += segmentSum;
    }
    
    cout << result << endl;
    return 0;
}

Problem 3: Interval Counting with Dual Constraints

This problem asks us to count valid intervals [i, j] where both endpoints and values satisfy certain constraints. The structure suggests a 2D point counting approach.

First, we use monotonic stacks to compute for each position i, the maximum extent L[i] it can extend leftward and R[i] it can extend rightward while maintaining valid monotonicity conditions.

For left endpoint constraints, we constructt points (i, Li) and query rectangles [Ri, i] × [n+1, n+1]. Similarly, for right endpoint constraints, we transform coordinates and query the corresponding regions.

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 200005;

int n, queryId;
int arr[MAXN];

struct FenwickTree {
    int bit[MAXN];
    
    void update(int idx, int delta) {
        if (idx == 0) return;
        for (; idx < MAXN; idx += idx & -idx) {
            bit[idx] += delta;
        }
    }
    
    int query(int idx) {
        int res = 0;
        for (; idx > 0; idx -= idx & -idx) {
            res += bit[idx];
        }
        return res;
    }
    
    void reset() {
        memset(bit, 0, sizeof(bit));
    }
} fenwick;

struct Point {
    int type;
    int x, y;
    int value;
    int idx;
};

int leftBound[MAXN], rightBound[MAXN];
int Lrange[MAXN], Rrange[MAXN];
Point points[MAXN * 2];
int pointCount;

int answerLeft[MAXN], answerRight[MAXN];
int stackArr[MAXN], stackTop;

bool comparePoints(const Point& a, const Point& b) {
    if (a.x != b.x) return a.x < b.x;
    if (a.type != b.type) return a.type < b.type;
    return a.y < b.y;
}

void computeBoundaries() {
    // Lmax: leftmost position with strictly smaller value
    stackTop = 0;
    for (int i = 1; i <= n; i++) {
        while (stackTop > 0 && arr[stackArr[stackTop]] < arr[i]) {
            stackTop--;
        }
        Lrange[i] = stackArr[stackTop];
        stackArr[++stackTop] = i;
    }
    
    // Lmin: leftmost position with strictly larger value
    stackTop = 0;
    for (int i = 1; i <= n; i++) {
        while (stackTop > 0 && arr[stackArr[stackTop]] > arr[i]) {
            stackTop--;
        }
        leftBound[i] = stackArr[stackTop];
        stackArr[++stackTop] = i;
    }
    
    for (int i = 1; i <= n; i++) {
        Lrange[i] = min(Lrange[i], leftBound[i]);
    }
    
    // Rmax and Rmin for right boundaries
    stackTop = 0;
    stackArr[stackTop] = n + 1;
    for (int i = n; i >= 1; i--) {
        while (stackTop > 0 && arr[stackArr[stackTop]] < arr[i]) {
            stackTop--;
        }
        rightBound[i] = stackArr[stackTop];
        stackArr[++stackTop] = i;
    }
    
    stackTop = 0;
    stackArr[stackTop] = n + 1;
    for (int i = n; i >= 1; i--) {
        while (stackTop > 0 && arr[stackArr[stackTop]] > arr[i]) {
            stackTop--;
        }
        Rrange[i] = stackArr[stackTop];
        stackArr[++stackTop] = i;
    }
    
    for (int i = 1; i <= n; i++) {
        Rrange[i] = max(rightBound[i], Rrange[i]);
    }
}

void solveRightEndpoint() {
    pointCount = 0;
    
    for (int i = 1; i <= n; i++) {
        points[++pointCount] = {0, i, Rrange[i] + 1, 0, 0};
    }
    
    for (int i = 1; i <= n; i++) {
        if (Lrange[i] >= 1) {
            points[++pointCount] = {1, Lrange[i], i + 1, 1, i};
        }
    }
    
    stable_sort(points + 1, points + pointCount + 1, comparePoints);
    fenwick.reset();
    
    for (int i = 1; i <= pointCount; i++) {
        if (points[i].type == 0) {
            fenwick.update(points[i].y, 1);
        } else {
            answerRight[points[i].idx] += 
                points[i].value * fenwick.query(points[i].y);
        }
    }
}

void solveLeftEndpoint() {
    pointCount = 0;
    
    for (int i = 1; i <= n; i++) {
        int transformedX = n + 1 - i;
        int transformedY = n + 1 - Lrange[i] + 1;
        points[++pointCount] = {0, transformedX, transformedY, 0, 0};
        
        if (n + 1 - Rrange[i] >= 1) {
            int endX = n + 1 - Rrange[i];
            int endY = n + 1 - i + 1;
            points[++pointCount] = {1, endX, endY, 1, i};
        }
    }
    
    stable_sort(points + 1, points + pointCount + 1, comparePoints);
    fenwick.reset();
    
    for (int i = 1; i <= pointCount; i++) {
        if (points[i].type == 0) {
            fenwick.update(points[i].y, 1);
        } else {
            answerLeft[points[i].idx] += 
                points[i].value * fenwick.query(points[i].y);
        }
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    cin >> n >> queryId;
    for (int i = 1; i <= n; i++) {
        cin >> arr[i];
    }
    
    computeBoundaries();
    solveRightEndpoint();
    solveLeftEndpoint();
    
    for (int i = 1; i <= n; i++) {
        cout << answerLeft[i] << ' ';
    }
    cout << '\n';
    
    for (int i = 1; i <= n; i++) {
        cout << answerRight[i] << ' ';
    }
    cout << '\n';
    
    return 0;
}

Note: Using stable_sort proves necessary for passing all test cases, whereas regular sort may produce incorrect results due to subtle ordering dependencies in the coordinate transformation.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.