- problem
  
- solution
  
 	R(n), R(m); int L = 0;
    F(i, 1, m) R(d[i].st), R(d[i].en), c[++ L] = d[i].st, c[++ L] = d[i].en;
 
    c[++ L] = n;
    sort(c + 1, c + L + 1); int cnt = 0;
    F(i, 1, L) if (c[i] != c[i - 1]) {
        g[c[i]] = ++ cnt;
        D[cnt] = c[i];
    }
 
    sort(d + 1, d + m + 1);
 
    f[0][0][0] = 1;
    F(i, 0, m - 1)
        F(j, 0, cnt)
            F(k, 0, cnt) if (f[i][j][k]) {
                add(f[i + 1][j][k], f[i][j][k]);
 
                int l = D[j], r = D[k];
                int l_ = g[d[i + 1].st], r_ = g[d[i + 1].en];
 
                if (d[i + 1].st - 1 <= l) {
                    add(f[i + 1][max(j, min(k, r_))][max(k, r_)], f[i][j][k]);
                }
            }
 
    W(f[m][cnt][cnt]);
 
  
  
  
- 线段树优化DP
  
 
  
  
  
  
- Problem 

这题题目需要注意就是每个节点都应该有2个儿子(他说没有叶子结点是错的,应该是要么没有儿子,要么有两个儿子),然后要求是到达每个叶子结点需要经过的向左的边不能超过M条.
- Solution - 容易想到简单的计数Dp,f[i][j]表示当前有i个叶节点,最多的向左走了M次,O(n^3) - 非常妙的优化是考虑从dfs序角度DP,因为每一棵树都唯一对应着一个DFS序。  
  
- problem
  
- solution

在合并括号序列时,先关注当前最后一个括号所配对到的位置,考虑求这一部分的方案数,再考虑剩下的部分方案(可以再递归去求方案)。
只能说不看这代码根本不知道这题解想要表达什么,具体想法参见代码,真的非常妙
void DFS(int u, int dep, vector<int> *G, vector<int> *tt, int *ttt) {
  tt[dep].push_back(u);
  ttt[u] = tt[dep].size() - 1;
  for (int v: G[u])
    DFS(v, dep + 1, G, tt, ttt);
  return;
}
int ct = 0;
int Solve(int nw1, int nw2, int lp1, int rp1, int lp2, int rp2) {
  if (lp1 == -1 || lp2 == -1)return 1;
  if (lp1 > rp1 || lp2 > rp2)return 1;
  int l1 = bin1[nw1][lp1], r1 = bin1[nw1][rp1]; // bin1[nw1][x] 表示第一棵树第nw1层的第x个节点
  int l2 = bin2[nw2][lp2], r2 = bin2[nw2][rp2]; // bin2[nw2][x] 表示第二棵树第nw2层的第x个节点
  if (vis[l1][r1][l2][r2])
    return dp[l1][r1][l2][r2];
  LL res = 0;
  ++ ct;
  Add(res, Solve(nw1, nw2, lp1, rp1 - 1, lp2, rp2)); // 当前序列后面没有括号要合并了,第一课树的第rp1对应节点作为最外层括号。
  Add(res, Solve(nw1, nw2, lp1, rp1, lp2, rp2 - 1));
// 下面表示要合并括号的。
  int ll = 0, rr = 0;
  if (G2[r2].size())ll = id2[G2[r2][0]], rr = id2[G2[r2].back()];
  else ll = rr = -1;
  for (int i = 1; i <= rp1 - lp1 + 1; i++)
    Add(res, 1ll * Solve(nw1, nw2, lp1, rp1 - i, lp2, rp2 - 1) * Solve(nw1, nw2 + 1,
                                                                       rp1 - i + 1, rp1, ll, rr) % mod); // 表示当前最后一个括号是第二棵树的, rp1-i就表示用第一棵树当前深度的后面i个作为后缀,
  if (G1[r1].size())ll = id1[G1[r1][0]], rr = id1[G1[r1].back()];
  else ll = rr = -1;
  for (int i = 1; i <= rp2 - lp2 + 1; i++)
    Add(res, 1ll * Solve(nw1, nw2, lp1, rp1 - 1, lp2, rp2 - i) * Solve(nw1 + 1, nw2,
                                                                       ll, rr, rp2 - i + 1, rp2) % mod);
  vis[l1][r1][l2][r2] = 1;
  dp[l1][r1][l2][r2] = res;
  return res;
}
int main() {
  n1 = read();
  for (int i = 2; i <= n1; i++)
    G1[read()].push_back(i);
  n2 = read();
  for (int i = 2; i <= n2; i++)
    G2[read()].push_back(i);
  DFS(1, 0, G1, bin1, id1);
  DFS(1, 0, G2, bin2, id2);
  LL ans = 0;
  if (G1[1].size()) {
    Add(ans, Solve(1, 0, id1[G1[1][0]], id1[G1[1].back()], 0, 0)); // 最外层括号是第一棵树的根节点
  } else Add(ans, 1); // 否则第二棵树的括号序只能不变的放到最外括号的里边
  if (G2[1].size()) {
    Add(ans, Solve(0, 1, 0, 0, id2[G2[1][0]], id2[G2[1].back()])); // 同理,最外层括号是第二棵树的根节点
  } else Add(ans, 1);
  cout << ans;
Problem

Solution

这里的实现参考:
for(int i=1;i<=n;i++) {
    for(int j=1;j<=i;j++)
        if(id[i]==1||id[i]==n) {
            trans(dp[i][j],dp[i-1][j-1],-x[id[i]]-y[id[i]],1);
            if(j)
                trans(dp[i][j],dp[i-1][j],x[id[i]]+y[id[i]],1);
            // 这里有点运用插空法的思想,头和尾的位置是固定的,所以方案乘1,其余是有几个空格就有乘多少方案,这样算起来容易太多了
        }
        else {
            if (j - tot > 0)
                trans(dp[i][j],dp[i-1][j-1],-2*x[id[i]]-2*y[id[i]],j-tot); // 这里就是j-1个联通块,有j个空,减去tot个(头尾影响),这里相当于作为一个独立的联通块加进去
            if (2 * j - tot > 0)
                trans(dp[i][j],dp[i-1][j],0,2*j-tot); // 这里是和之前的联通块合并,之前每个联通块都有头和尾,这个可以放到头或尾,同理要减去tot(不能放到st的头/en的尾)
            trans(dp[i][j],dp[i-1][j+1],2*x[id[i]]+2*y[id[i]],j); // 这里是合并两个联通块,j+1个联通块,当然只能有j个地方可以连,这是因为这些联通块的顺序已经相对确定了。
        }
    tot+=(id[i]==1||id[i]==n);
}