动态规划 刷题记录
AT_DP_C Vacation
洛谷 Link
    
      
        
▶题意      
      
        
          
有 NNN 天,对于每一天 iii(1≤i≤N1 \leq i \leq N1≤i≤N),可以选择以下活动之一:
A:在海里游泳,获得幸福度 aia _ iai。
B:在山上抓虫,获得幸福度 bib _ ibi。
C:在家做作业,获得幸福度 cic _ ici。
不能连续两天以上做同样的活动,计算最大总幸福度。
        
      
    
Solution
DP 板子题。设 fi,jf_{i,j}fi,j 表示截止到第 iii 天时做第 jjj 件事的幸福值总和。
则易得转移方程为:
fi,1=max(fi−1,2,fi−1,3)fi,2=max(fi−1,1,fi−1,3)fi,3=max(fi−1,1,fi−1,2)f_{i,1}=\max(f_{i-1,2},f_{i-1,3})\\f_{i,2}=\max(f_{i-1,1},f_{i-1,3})\\f_{i,3}=\max(f_{i-1,1},f_{i-1,2})fi,1=max(fi−1,2,fi−1,3)fi,2=max(fi−1,1,fi−1,3)fi,3=max(fi−1,1,fi−1,2)
Core Code
1
2
3
4
5
6
for(int i=1;i<=n;i++)
{
    f[i][1]=max(f[i-1][2],f[i-1][3])+a[i];
    f[i][2]=max(f[i-1][1],f[i-1][3])+b[i];
    f[i][3]=max(f[i-1][1],f[i-1][2])+c[i];
}
AT_DP_K Stones
洛谷 Link
    
      
        
▶题意      
      
        
          
NNN 个正整数组成的集合 A={a1,a2,…,aN}A = \{ a _ 1, a _ 2, \ldots, a _ N \}A={a1,a2,…,aN}。太郎君和次郎君将用以下游戏进行对决。
首先,准备一个有 KKK 个石子的堆。两人依次进行以下操作。太郎君先手。
从集合 AAA 中选择一个元素 xxx,从石堆中恰好移除 xxx 个石子。
不能进行操作的人输掉游戏。当两人都按照最优策略行动时,判断谁会获胜。
        
      
    
Solution
显然当一名玩家操作完成后石子数量为 000,则这名玩家必胜。
设 fif_ifi 表示剩余 iii 枚石子当前操作的人的输赢情况 (1/01/01/0),可以发现当且仅当当前操作的上一步操作必输,当前操作才可以必胜,即如果有 aja_jaj 使得 fi−aj=0f_{i-a_j}=0fi−aj=0,则 fi=1f_i=1fi=1。
初始化 f0=0f_0=0f0=0,则有:
fi={1,aj≤i,fi−aj=00,otherwise.f_i=\begin{cases}1,&a_j\leq i,f_{i-a_j}=0\\0 ,& \text{otherwise.}\end{cases}fi={1,0,aj≤i,fi−aj=0otherwise.
Core Code
1
2
3
4
5
6
7
8
f[0]=0;
for(int i=1;i<=k;i++)
{
    for(int j=1;j<=n;j++)
    {
        if(i-a[j]>=0 && !f[i-a[j]]) f[i]=1;
    }
}
AT_DP_M Candies
Link
KKK 颗糖分给 nnn 个人,第 iii 个人至少分得 000 颗,至多分得 aia_iai 颗,必须分完,求方案数,答案对 109+710^9+7109+7 取模。
tag: 背包 DP, 前缀和
Solution
类似背包的处理思路,设 fi,jf_{i,j}fi,j 表示第 iii 个人分完后已经分了 jjj 个糖,它可以由上一步的 (j−ai)∼j(j-a_i)\sim j(j−ai)∼j 转移得到,所以
fi,j=∑k=max(0,j−ai)jfi−1,kf_{i,j}=\sum^j_{k=\max(0,j-a_i)}f_{i-1,k}fi,j=k=max(0,j−ai)∑jfi−1,k
考虑优化时间,滚动数组不可行。发现每一个 iii 对应的值都由 i−1i-1i−1 中连续一段的值转移而来,想到用滚动的前缀和维护 i−1i-1i−1 的所有 fff 值。
Core Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int calc(int l,int r) {
    if(l==0) return sum[r];
    return sum[r]-sum[l-1];
}
int main() {
    f[0][0]=1;
    for(int i=1;i<=n;i++) {
        sum[0]=f[i-1][0];
        for(int j=1;j<=m;j++) {
            sum[j]=(sum[j-1]+f[i-1][j]+mod)%mod;
        }
        for(int j=0;j<=m;j++) {
            f[i][j]=(calc(max(0ll,j-a[i]),j)+mod)%mod;
        }
    }
    printf("%lld\n",f[n][m]%mod);
}
AT_DP_P Independent Set
Link
给一棵树,每一个点可以染成黑色或白色,任意两个相邻节点不能都是黑色,求方案数。
tag: 树形 DP
Solution
设 fu,0/1f_{u,0/1}fu,0/1 表示 uuu 节点染成 W/B 颜色时的方案数。DFS 子树的方案数,可以得到:
fu,0=∏v∈u(fv,0+fv,1)fu,1=∏v∈ufv,0\begin{aligned}f_{u,0}&=\prod_{v\in u}(f_{v,0}+f_{v,1})\\f_{u,1}&=\prod_{v\in u}f_{v,0}\end{aligned}fu,0fu,1=v∈u∏(fv,0+fv,1)=v∈u∏fv,0
注意 111 号点为根,输入无序需建双向边。
Core Code
1
2
3
4
5
6
7
8
9
10
11
12
void dfs(int u) {
    g.vis[u]=1;
    f[u][0]=f[u][1]=1;
    for(int i=0;i<e[u].size();i++) {
        int v=e[u][i];
        if(!g.vis[v]) {
            dfs(v);
            f[u][0]=(f[u][0]*(f[v][0]+f[v][1]))%mod;
            f[u][1]=(f[u][1]*f[v][0])%mod;
        }
    }
}
AT_DP_Q Flowers
Link
有一排花,共 nnn 个,第 iii 个的高度是 hih_ihi,权值是 aia_iai,保证高度互不相同。现在拿走一些花,使剩下的花高度单调递增,问剩下的花权值之和最大是多少。
tag: 线段树优化 DP
Solution
带权的最长上升子序列,设 fif_ifi 表示第 iii 个元素结尾的最大答案,可以参照最长上升子序列写出转移方程
fi=maxj=1i−1{fj∣hj<hi}+aif_{i}=\max_{j=1}^{i-1}\{f_{j}|h_j<h_i\}+a_{i}fi=j=1maxi−1{fj∣hj<hi}+ai
考虑优化,观察到转移方程是区间最大值,想到用值域线段树优化。将 hih_ihi 作为线段树的下标,对应当前高度时的 fif_ifi。
Core Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
struct node {
    ll val;
}t[N * 4];
struct tree {
    void pushup(int p) {
        t[p].val = max(t[p * 2].val, t[p * 2 + 1].val);
    }
    void modify(int p, int l, int r, int x, ll k) {
        if (l == r) {
            t[p].val = k;
            return;
        }
        int mid = (l + r) >> 1;
        if (x <= mid) modify(p * 2, l, mid, x, k);
        else modify(p * 2 + 1, mid + 1, r, x, k);
        pushup(p);
    }
    ll query(int p, int l, int r, int x, int y) {
        if (x <= l && r <= y) {
            return t[p].val;
        }
        ll res = 0;
        int mid = (l + r) >> 1;
        if (x <= mid) res = max(res, query(p * 2, l, mid, x, y));
        if (y >= mid + 1) res = max(res, query(p * 2 + 1, mid + 1, r, x, y));
        return res;
    }
}sgt;
int main() {
    n = read();
    for (int i = 1; i <= n; i++) {
        h[i] = read();
    }
    for (int i = 1; i <= n; i++) {
        a[i] = read();
    }
    for (int i = 1; i <= n; i++) {
        f[i] = sgt.query(1, 1, n, 1, h[i]) + a[i];
        sgt.modify(1, 1, n, h[i], f[i]);
    }
    cout << sgt.query(1, 1, n, 1, n) << endl;
    return 0;
}
AT_DP_T Permutation
Link
有一个长为 NNN 的正整数排列。给定一个由 < 和 > 组成长为 N−1N-1N−1 的的字符串。对于任意满足 1≤i≤N−11 \le i \le N-11≤i≤N−1 的字符 sis_isi,如果 sis_isi 是 < 则 Pi<Pi+1P_i<P_{i+1}Pi<Pi+1、如果 sis_isi 是 > 则 Pi>Pi+1P_i>P_{i+1}Pi>Pi+1。求满足这样的性质的排列 PPP 的方案数。
Solution
由于 ppp 是排列,我们不在意 ppp 中到底放了什么数,只需要关注当前一步放的数和上一步放的数的大小关系。可以得到状态 fi,jf_{i, j}fi,j 表示前 iii 个数形成的排列中,第 iii 个数是第 jjj 大的方案数。
在转移时,若当前填的数需大于前一个数,那么直接得到 fi,j=∑k=1j−1fi−1,k\displaystyle f_{i,j}=\sum_{k=1}^{j-1}f_{i-1,k}fi,j=k=1∑j−1fi−1,k。
若小于前一个数,有 fi,j=∑k=ji−1fi−1,k\displaystyle f_{i,j}=\sum_{k=j}^{i-1}f_{i-1,k}fi,j=k=j∑i−1fi−1,k。可以感性理解,由于前面一定有一个 rank 为 jjj 的数,当前数扔进去的时候,相当于是扔到了原有排列(1∼i−11\sim i-11∼i−1)中 rank 为 j−1j-1j−1  和 jjj 中间,所以说应该加上所有 rank 大于等于 jjj 的贡献,即 j∼i−1j\sim i-1j∼i−1。
注意到转移是求连续一段的和,前缀和优化即可。
Core Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sum[1] = 1;
for (int i = 2; i <= n; i++) {
    for (int j = 1; j <= n; j++) {
        if (s[i - 1] == '<') {
            f[j] = (sum[i - 1] - sum[j - 1] + mod) % mod;
        } else {
            f[j] = sum[j - 1] % mod;
        }
    }
    for (int j = 1; j <= n; j++) {
        sum[j] = (sum[j - 1] + f[j]) % mod;
    }
}
int ans = 0;
for (int i = 1; i <= n; i++) {
    ans = (ans + f[i]) % mod;
}
cout << ans % mod << '\n';
AT_DP_W Intervals
Link
给定 mmm 条规则形如 (li,ri,ai)(l_i,r_i,a_i)(li,ri,ai),对于一个 01 串,其分数的定义是:对于第 iii 条规则,若该串在 [li,ri][l_i,r_i][li,ri] 中至少有一个 1,则该串的分数增加 aia_iai。
你需要求出长度为 nnn 的 01 串中的最大分数。
tag: 线段树优化 DP, 区间右端点排序
Solution
首先将区间按右端点排序,在右端点处统计答案。
考虑 DP,设 fi,jf_{i,j}fi,j 表示前 iii 个位置中最后一个 111 的位置为 jjj 的最大分数。
不难想到 fi,j=maxk=1jfi−1,k+∑lp≤k and rp=iap\displaystyle f_{i,j}=\max_{k=1}^j f_{i-1,k}+\sum_{l_p\leq k\text{ and }r_p=i} a_pfi,j=k=1maxjfi−1,k+lp≤k and rp=i∑ap。即使 [l,r][l, r][l,r] 覆盖到 kkk 位置。
对上式进行优化,发现可以贪心的使 111 的位置尽量靠后,才能最大化 111 的数量,所以 fi,j=fi−1,j+∑lk≤j and rk=iak\displaystyle f_{i,j}=f_{i-1,j}+\sum_{l_k\leq j\text{ and }r_k=i}a_kfi,j=fi−1,j+lk≤j and rk=i∑ak。
优化后,会有 j=ij=ij=i 的特殊情况,此时上一步选的 111 可以在任意位置,取最大值即可。最终转移方程即为:
fi,j={maxk=1j−1fi−1,k+∑rp=iap,i=jfi−1,j+∑lk≤j and rk=iak,i≠jf_{i,j}=\begin{cases}\max_{k=1}^{j-1} f_{i-1,k}+\sum_{r_p=i} a_p,&i=j\\f_{i-1,j}+\sum_{l_k\leq j\text{ and }r_k=i} a_k,&i\neq j\end{cases}fi,j={maxk=1j−1fi−1,k+∑rp=iap,fi−1,j+∑lk≤j and rk=iak,i=ji=j
滚动掉一维后,发现枚举位置 iii 时,如果 rj=ir_j=irj=i,那么这段区间 [lj,rj][l_j, r_j][lj,rj] 会在 [lj,i][l_j, i][lj,i] 中做出 aja_jaj 的贡献。扔到线段树上区间加区间查询最大值即可。
Core Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
struct query {
    int l, r, a;
    bool operator<(const query &x) const {
        return r < x.r;
    }
}q[N];
struct node {
    int val, tag;
};
struct tree {
    node t[4 * N];
    void mktag(int p, int w) {
        t[p].val += w;
        t[p].tag += w;
    }
    void pushdown(int p) {
        if (t[p].tag) {
            mktag(p * 2, t[p].tag);
            mktag(p * 2 + 1, t[p].tag);
            t[p].tag = 0;
        }
    }
    void pushup(int p) {
        t[p].val = max(t[p * 2].val, t[p * 2 + 1].val);
    }
    void modify(int p, int l, int r, int x, int y, int w) {
        if (x <= l && r <= y) {
            mktag(p, w);
            return;
        }
        pushdown(p);
        int mid = (l + r) >> 1;
        if (x <= mid) modify(p * 2, l, mid, x, y, w);
        if (y >= mid + 1) modify(p * 2 + 1, mid + 1, r, x, y, w);
        pushup(p);
    }
    int query(int p, int l, int r, int x, int y) {
        if (x <= l && r <= y) {
            return t[p].val;
        }
        pushdown(p);
        int mid = (l + r) >> 1;
        int res = 0;
        if (x <= mid) res = max(res, query(p * 2, l, mid, x, y));
        if (y >= mid + 1) res = max(res, query(p * 2 + 1, mid + 1, r, x, y));
        return res;
    }
}sgt;
signed main() {
    n = read(); m = read();
    for (int i = 1; i <= m; i++) {
        q[i].l = read(), q[i].r = read(), q[i].a = read();
    }
    sort(q + 1, q + m + 1);
    int nw = 1;
    for (int i = 1; i <= n; i++) {
        if (i != 1) sgt.modify(1, 1, n, i, i, sgt.query(1, 1, n, 1, i - 1));
        while (q[nw].r == i && nw <= m) {
            sgt.modify(1, 1, n, q[nw].l, i, q[nw].a);
            nw++;
        }
    }
    cout << max(0ll, sgt.query(1, 1, n, 1, n)) << endl;
    return 0;
}
TDPC_IWI イウィ
洛谷 Link
给定一个仅由字符 i\texttt{i}i 和 w\texttt{w}w 构成的字符串 sss。你可以进行若干次操作,每次从串中选取连续的三个字符 iwi\texttt{iwi}iwi 并删除;每次操作后这三个字符的左侧和右侧会连接在一起,得到一个长度比原来小 333 的新串。求可能的最大操作次数。
tag: 区间 DP
Solution
区间 DP,类似合并石子。
我们定义 fi,jf_{i,j}fi,j 记录 iii 到 jjj 能删掉多少字符,显然最后答案应除以 333。
因为答案计算的是最大的步数,那么对于一个大区间,在一般情况下的答案就应该是其分成的两个小区间的答案之和。考虑枚举这两个区间之间的分界点 kkk,答案取最大值即可。
本题还需要特判一种特殊情况。如果 sis_isi 和 sjs_jsj 为 i,sks_ksk 为 w,且中间的两个小段可以被完全删除。那么,当中间的被删掉之后,左右的 i 和中间的 w 就会拼在一起,也可以删掉,那么这个区间的答案就应该是它的长度,但显然直接相加有可能会出错。
Core Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for(int a=2;a<=len;a++)
{
    for(int i=1;a+i-1<=len;i++)
    {
        int j=a+i-1;
        for(int k=i;k<j;k++)
        {
            f[i][j]=max(f[i][j],f[i][k]+f[k+1][j]);
            if(s[i]=='i' && s[j]=='i' && s[k]=='w' && f[i+1][k-1] == k-i-1 && f[k+1][j-1] == j-1-k)
            {
                f[i][j]=j-i+1;
            }
        }
    }
}
write(f[1][len]/3);
ABC247F Cards
Link
给定 nnn 张卡片,每张卡片正反面各有一个数,给定每张卡片正面和反面的数,保证正面的数构成的序列,和反面的数构成的,分别均为 111 到 nnn 的排列,可以选择任意张卡片并获得其正反面的数,要求最终所有获得的数至少包含 111 到 nnn 每个数至少一次。求有多少种取法,对 998244353998244353998244353 取模。
tag: 并查集
Solution
trick 题,记住。
想到在两个拥有相同数字的卡片连边,则一个边的两点中必须至少选一个。
由于每张卡有两个数,度数为二,则连完后必定会形成若干个简单环或自环。
根据乘法原理,对于每个环求出方案数后相乘后即为答案。
先上结论,设 fif_ifi 表示一个大小为 iii 的环的选择方案数,有如下转移:
fi=fi−1+fi−2f_i=f_{i-1}+f_{i-2}fi=fi−1+fi−2
考虑证明它,设一个如下图的环,其中 others 表示剩余的所有点。
分类讨论,如果选 AAA 点,那么 BBB 和 CCC 点可以任取。转化为剩余一条不含 AAA 的链,这样不好考虑,将 BBB 和 CCC 连接起来,转化为少一个点的环,这个环的答案是 fi−1f_{i-1}fi−1。但这样还少了一种 BBB 和 CCC 都不算的情况,考虑将 BBB 和 CCC 缩为一点,将个大点不选的方案数位 kkk。
如果不选 AAA 点,那么还是上面的链,但 BBB 和 CCC 必选,按照上面的处理方式,同样将 BBB 和 CCC 缩为一点,这个图的方案数是 fi−2f_{i-2}fi−2。但这样多算了 BBB 和 CCC 都不选的贡献,可以发现这部分贡献就是 kkk。
所以可以得到:
fi=fi−1+k̸+fi−2−k̸f_i=f_{i-1}+\not k+f_{i-2}-\not kfi=fi−1+k+fi−2−k
用并查集存环即可。
Core Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
n = read();
for (int i = 1; i <= n; i++) fa[i] = i;
for (int i = 1; i <= n; i++) {
    t[read()] = i;
}
for (int i = 1; i <= n; i++) {
    q[i] = read();
    merge(i, t[ q[i] ]);
}
for (int i = 1; i <= n; i++) {
    siz[find(i)]++;
}
f[1] = 1ll, f[2] = 3ll;
for (int i = 3; i < N; i++) {
    f[i] = (f[i - 1] + f[i - 2]) % mod;
}
int ans = 1ll;
f[0] = 1ll;
for (int i = 1; i <= n; i++) {
    ans = (ans * f[ siz[i] ] % mod + mod) % mod;
}
ABC336D Pyramid
Link
    
      
        
▶题意      
      
        
          
对于正整数 kkk,一个大小为 kkk 的“金字塔数列”为一个长度为 2k−12k-12k−1 的数列,里面的数字依次为 1,2,3,…k−1,k,k−1,…3,2,11,2,3,\dots k-1,k,k-1,\dots 3,2,11,2,3,…k−1,k,k−1,…3,2,1。
现在给一个长度为 nnn 的数列 SSS,你可以进行以下操作任意次,使得数列最后变为一个“金字塔数列”:
选择一个数 i(1≤i≤n)i(1 \le i \le n)i(1≤i≤n),把 SiS_iSi 减少 111。
删除整个数列的第一个或最后一个数字。
问最后生成的“金字塔数列”的最大的 kkk 是多少。
        
      
    
Solution
显然一个金字塔数列是由左右两部分拼接而成的。设 fif_{i}fi 表示前 iii 个数能组成左半部分的最长长度,gig_{i}gi 表示以 iii 为首的序列能组成右半部分的最长长度(差不多等价于后 iii 个数,但注意转移方程的写法)。一个明显的性质是如果满足可以构成长度为 xxx 的序列,那么一定可以通过删掉元素构成长度为 x−1x-1x−1 的序列,所以可以发现以 iii 为中间项的最长长度即为 min{fi,gi}\min\{f_i, g_i\}min{fi,gi}。
以 fff 为例,假设 sis_isi 的所有值都是 +∞+\infty+∞,可以得到显然的转移方程 fi=fi−1+1f_{i}=f_{i-1}+1fi=fi−1+1。如果加上 sis_isi 的限制条件,那么 fi=min{fi−1+1,si}f_i=\min\{f_{i-1}+1, s_i\}fi=min{fi−1+1,si}。同理可得 gi=min{gi+1+1,si}g_i=\min\{g_{i+1}+1, s_i\}gi=min{gi+1+1,si}。
Core Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
n = read();
for (int i = 1; i <= n; i++) {
    a[i] = read();
}
f[1] = g[n] = 1;
for (int i = 2; i <= n; i++) {
    f[i] = min(f[i-1] + 1, a[i]);
}
for (int i = n - 1; i >= 1; i--) {
    g[i] = min(g[i+1] + 1, a[i]);
}
int ans = 0;
for (int i = 1; i <= n; i++) {
    ans = max(ans, min(f[i], g[i]));
}
cout << ans << endl;
SP703 Mobile Service
Problem Link
    
      
        
▶题意      
      
        
          
有 333 个流动员工,任何时刻只有一名员工可以移动,不允许同一位置上有 222 个及以上员工。
从位置 ppp 移动到位置 qqq 需要花费 c(p,q)c(p,q)c(p,q) 的价钱,不移动不需要花费(即 c(i,i)=0c(i,i)=0c(i,i)=0 )但不保证 c(p,q)=c(q,p)c(p,q)=c(q,p)c(p,q)=c(q,p)。
现在给出  NNN 个请求,第 iii 个请求发生在位置 xix_ixi。公司必须按照顺序,派一名员工到位置 xix_ixi,过程中不能去其他地方,也就是必须直接过去。
333 个流动员工的初始位置分别为 1,2,31,2,31,2,3。
求公司的最小花费。
        
      
    
tag: DP
Solution
设 fi,x,y,zf_{i,x,y,z}fi,x,y,z 表示处理到第 iii 个请求,三个人分别在 x,y,zx,y,zx,y,z 位置上时的最小值。注意到三个人中一定会有一个人在上一个请求的位置,故状态可以优化为 fi,x,yf_{i,x,y}fi,x,y,此时令 z=qi−1z=q_{i-1}z=qi−1 则有
fi+1,x,y=min(fi+1,x,y,fi,x,y+cz,qi+1)fi+1,x,z=min(fi+1,x,z,fi,x,y+cy,qi+1)fi+1,y,z=min(fi+1,y,z,fi,x,y+cx,qi+1)f_{i+1,x,y}=\min(f_{i+1,x,y},f_{i,x,y}+c_{z,q_{i+1}})\\f_{i+1,x,z}=\min(f_{i+1,x,z},f_{i,x,y}+c_{y,q_{i+1}})\\f_{i+1,y,z}=\min(f_{i+1,y,z},f_{i,x,y}+c_{x,q_{i+1}})fi+1,x,y=min(fi+1,x,y,fi,x,y+cz,qi+1)fi+1,x,z=min(fi+1,x,z,fi,x,y+cy,qi+1)fi+1,y,z=min(fi+1,y,z,fi,x,y+cx,qi+1)
初值 q0=1,f0,2,3=0q_0=1, f_{0,2,3}=0q0=1,f0,2,3=0。
Core Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
for(int i=0;i<n;i++) {
    for(int x=1;x<=l;x++) {
        for(int y=1;y<=l;y++) {
            int z=q[i];
            if(x==y || x==z || y==z) continue;
            f[i+1][x][y]=min(f[i+1][x][y],f[i][x][y]+c[z][q[i+1]]);
            f[i+1][z][y]=min(f[i+1][z][y],f[i][x][y]+c[x][q[i+1]]);
            f[i+1][x][z]=min(f[i+1][x][z],f[i][x][y]+c[y][q[i+1]]);
        }
    }
}
int ans=INF;
for(int x=1;x<=l;x++) {
    for(int y=1;y<=l;y++) {
        int z=q[n];
        if(x==y || x==z || y==z) continue;
        ans=min(ans,f[n][x][y]);
    }
}
P4310 绝世好题
Link 和同类题 CF264B Good Sequences
给定一个长度为 nnn 的数列 aia_iai,求 aia_iai 的子序列 bib_ibi 的最长长度 kkk,满足 bi and bi−1≠0b_i\ \text{and}\ b_{i-1} \neq 0bi and bi−1=0,其中 2≤i≤k2\leq i\leq k2≤i≤k。
tag: 位运算, DP
Solution
设 fif_{i}fi 表示最后一个数为 aia_iai 的最长长度,biti\text{bit}_ibiti 表示遍历到当前数二进制位 iii 为 111 的数量。
可以注意到只要两个数同一二进制位都为 111,那么他们与起来一定不等于 000。同时可以注意到上面的结论还可以传递,比如 (1000)2 and (1010)2 and (0010)2≠0(1000)_2\ \text{and}\ (1010)_2\ \text{and}\ (0010)_2\neq 0(1000)2 and (1010)2 and (0010)2=0。
所以在每次循环时,先通过枚举 aia_iai 每一个二进制位更新 fif_ifi,然后用当前情况的 fif_ifi 去更新 aia_iai 二进制为为 111 的 biti\text{bit}_ibiti。
{fi=max(fi,bitj)bitj=fi,ai and (1<<j)≠0\begin{cases}f_i&=\max(f_i, \text{bit}_j)\\\text{bit}_j&=f_i\end{cases}, a_i\ \text{and}\ (1<<j)\neq 0{fibitj=max(fi,bitj)=fi,ai and (1<<j)=0
Core Code
1
2
3
4
5
6
7
8
9
10
11
12
for(int i=1;i<=n;i++) {
    for(int j=0;j<=32;j++) {
        if((1<<j)&a[i]) f[i]=max(f[i],bit[j]+1);
    }
    for(int j=0;j<=32;j++) {
        if((1<<j)&a[i]) bit[j]=f[i];
    }
}
int ans=0;
for(int i=1;i<=n;i++) {
    ans=max(ans,f[i]);
}
P1944 最长括号匹配
Problem Link
    
      
        
▶题意      
      
        
          
对一个由 (,),[,] 括号组成的字符串,求出其中最长的括号匹配子串。
(),[] 是括号匹配的字符串。
若 A 是括号匹配的串,则 (A),[A] 是括号匹配的字符串。
若 A,B 是括号匹配的字符串,则 AB 也是括号匹配的字符串。
例如:(),[],([]),()() 都是括号匹配的字符串,而 ][,[(]) 则不是。
字符串 A 的子串是指由 A 中连续若干个字符组成的字符串,包含空串。
        
      
    
Solution
设 fif_ifi 表示到 sis_isi 时最长括号匹配,从头开始线性扫描,扫到后半括号时判断是否能组成匹配。
显然,当且仅当在其 前面括号匹配长度(fi−1f_{i-1}fi−1)的前一个字符是同类型的前半括号(即类似
(xxx))。
由此可得式子 fi=fi−1+fi−fi−1−2+2f_i=f_{i-1}+f_{i-f_{i-1}-2}+2fi=fi−1+fi−fi−1−2+2。其中 i−fi−1−2i-f_{i-1}-2i−fi−1−2 表示前半括号。
Core Code
1
2
3
4
5
6
7
8
9
10
11
for(int i=1;i<=n;i++) {
    if((s[i]==')' && s[i-f[i-1]-1]=='(') || (s[i]==']' && s[i-f[i-1]-1]=='[')) f[i]=f[i-1]+f[i-f[i-1]-2]+2;
    if(ans<f[i]) {
        ans=f[i];
        last=i;
    }
}
for(int i=last-ans+1;i<=last;i++) {
    putchar(s[i]);
}
el;
P1284 三角形牧场
Problem Link
给定多个线段,求全部使用情况下最大能围成的三角形周长。
tag: 背包
Solution
显然要用海伦公式求周长,那就需要枚举三角形三条边的长度。由于所有边都需要使用,可以优化为仅枚举两条边的长度。
设 fk,i,jf_{k,i,j}fk,i,j 表示第 kkk 个边时,是否存在一个两边长为 i,ji, ji,j 的三角形。
类似 01 背包思路,fkf_kfk 可以由 fk−1f_{k-1}fk−1 转移而来,当前的边可以加在 i,ji,ji,j 和第三条边上,也就可以由 fk−1,i−lk,j,fk−1,i,j−lk,fk−1,i,jf_{k-1, i-l_k, j}, f_{k-1, i, j-l_k}, f_{k-1, i, j}fk−1,i−lk,j,fk−1,i,j−lk,fk−1,i,j 转移而来
fk,i,j={true,fk−1,i−lk,j=true or fk−1,i,j−lk=true or fk−1,i,j=truefalse,otherwise.f_{k,i,j}=\begin{cases}\text{true}, &f_{k-1,i-l_k,j}=\text{true}\ \text{or}\ f_{k-1,i,j-l_k}=\text{true}\\text{or}\ f_{k-1,i,j}=\text{true}\\\text{false}, &\text{otherwise.}\end{cases}fk,i,j={true,false,fk−1,i−lk,j=true or fk−1,i,j−lk=true or fk−1,i,j=trueotherwise.
同 01 背包一样,kkk 这一维可以滚动掉。
Core Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
bool check(int a,int b,int c) {
    if(a+b>c && a+c>b && b+c>a) return 1;
    else return 0;
}
double calc(double a,double b,double c) {
    double p=(a+b+c)/2.0;
    return 100.0*sqrt(p*(p-a)*(p-b)*(p-c));
}
int main() {
    f[0][0]=1;
    for(int i=1;i<=n;i++) {
        for(int j=p;j>=0;j--) {
            for(int k=p;k>=0;k--) {
                if(j-l[i]>=0 && f[j-l[i]][k]) f[j][k]=1;
                if(k-l[i]>=0 && f[j][k-l[i]]) f[j][k]=1;
            }
        }
    }
    int ans=-1;
    for(int i=1;i<=p;i++) {
        for(int j=1;j<=p;j++) {
            if(check(i,j,p-i-j) && f[i][j]) ans=max(ans,(int)calc(i,j,p-i-j));
        }
    }
}
P4084 [USACO17DEC] Barn Painting G
Problem Link
给定一颗 NNN 个节点组成的树,你要给每个点涂上三种颜色之一,其中有 KKK 个节点已染色,要求任意两相邻节点颜色不同,求合法染色方案数。
tag: 树形 dp
Solution
树形 DP,设 fi,jf_{i,j}fi,j 为 iii 节点颜色 jjj 的方案数。
若 iii 已经被染色 jjj 了,那显然 fi,j=1f_{i,j}=1fi,j=1,其他为 000;若未被染色,则 fi,j=1,j∈[1,3]f_{i,j}=1, j\in [1,3]fi,j=1,j∈[1,3]。
从下向上转移:
{fu,1=∏v∈son(u)(fv,2+fv,3)fu,2=∏v∈son(u)(fv,1+fv,3)fu,3=∏v∈son(u)(fv,1+fv,2)\begin{cases}f_{u,1}=\prod_{v\in \text{son}(u)} (f_{v,2}+f_{v,3})\\f_{u,2}=\prod_{v\in \text{son}(u)} (f_{v,1}+f_{v,3})\\f_{u,3}=\prod_{v\in \text{son}(u)} (f_{v,1}+f_{v,2})\end{cases}⎩
⎨
⎧fu,1=∏v∈son(u)(fv,2+fv,3)fu,2=∏v∈son(u)(fv,1+fv,3)fu,3=∏v∈son(u)(fv,1+fv,2)
Core Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#define int ll
vector<int> e[N];
int n,k;
int f[N][5];
bool is_colored[N];
bool vis[N];
void dfs(int u) {
    vis[u]=1;
    for(int i=0;i<e[u].size();i++) {
        int v=e[u][i];
        if(!vis[v]) {
            dfs(v);
            f[u][1]=(f[u][1]*((f[v][2]+f[v][3]+mod)%mod)+mod)%mod;
            f[u][2]=(f[u][2]*((f[v][1]+f[v][3]+mod)%mod)+mod)%mod;
            f[u][3]=(f[u][3]*((f[v][1]+f[v][2]+mod)%mod)+mod)%mod;
        }
    }
}
signed main() {
    n=read();k=read();
    for(int i=1;i<n;i++) {
        int u=read(),v=read();
        e[u].push_back(v);
        e[v].push_back(u);
    }
    for(int i=1;i<=k;i++) {
        int b=read(),c=read();
        f[b][c]=1;
        is_colored[b]=1;
    }
    for(int i=1;i<=n;i++) {
        if(!is_colored[i]) f[i][1]=f[i][2]=f[i][3]=1;
    }
    dfs(1);
    printf("%lld\n",(f[1][1]+f[1][2]+f[1][3]+mod)%mod);
    return 0;
}
P2458 [SDOI2006] 保安站岗
Link
给定一颗树,每个点有点权 kkk,要求一条边的两个端点至少有一个被占用,求占用整棵树的代价的最小值。
tag: 树形 DP
与 P2016 战略游戏 不同,本题为守点。如 1-2-3-4 本题可以选择 1 和 4,但战略游戏不可。所以本题需要设这三个状态。
本题守点,意味着如果一个点不选,那么只需要与这个点相连的任一点选择即可,而守边则就需要与这个点相连的所有点都选。
Solution
首先不能将状态设为一个点被占或不占,这样会造成向上转移时父节点无法影响子节点。
设 fu,1/2/3f_{u,1/2/3}fu,1/2/3 表示 uuu 节点 自身被占用/被儿子覆盖/被父亲覆盖。
首先第一种情况显然 fu,1=∑v∈son(u)min(fv,1,fv,2,fv,3)+kuf_{u,1}=\sum_{v\in \text{son}(u)}\min(f_{v,1}, f_{v,2}, f_{v,3})+k_ufu,1=∑v∈son(u)min(fv,1,fv,2,fv,3)+ku。
对于第三种情况,由于 uuu 被父亲覆盖,自身未被占用,那么就需要使 以 uuu 的儿子 vvv 为根的子树中的点都被覆盖。问题转化后,只有 vvv 自身被占用和 vvv 被儿子覆盖可以满足条件,即 fu,3=∑v∈son(u)min(fv,1,fv,2)f_{u,3}=\sum_{v\in \text{son}(u)}\min(f_{v,1}, f_{v,2})fu,3=∑v∈son(u)min(fv,1,fv,2)。
对于第二种情况,分析方式类似第三种,首先要满足子树被覆盖。同时,要有至少一个 vvv 自身被占用,才能影响到 uuu。可以在上面的式子的基础上略加更改,若最小值全部被 fv,2f_{v,2}fv,2 取到,那么最后再加上所有 vvv 中 fv,1f_{v,1}fv,1 最小的一个即可。
最终答案即为 min(froot,1,froot,2)\min(f_{\text{root},1}, f_{\text{root},2})min(froot,1,froot,2)。
Core Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void dfs(int u,int fa) {
    int minn=INF;
    bool flag=0;
    for(auto v:e[u]) {
        if(v==fa) continue;
        dfs(v,u);
        f[u][1]+=min(f[v][1],min(f[v][2],f[v][3]));
        f[u][2]+=min(f[v][1],f[v][2]);
        f[u][3]+=min(f[v][1],f[v][2]);
        minn=min(minn,f[v][1]-f[v][2]);
        if(f[v][1]<f[v][2]) flag=1;
    }
    if(!flag) f[u][2]+=minn;
}
int main() {
    n=read();
    for(int i=1;i<=n;i++) {
        int u=read();
        k[u]=read();
        int m=read();
        f[u][1]=k[u];
        for(int j=1;j<=m;j++) {
            int v=read();
            e[u].push_back(v);
            e[v].push_back(u);
        }
    }
    dfs(1,0);
    cout<<min(f[1][1],f[1][2])<<endl;
    return 0;
}
P2016 战略游戏
Link
给定一颗树,一个节点可以看守相邻的所有边,求看守整棵树需要的最少的节点数。
tag: 树形 DP
本题与上一题不用的是,本题可以转化为一个点可以看守与这个点相连的所有边。
本题守边,意味着如果一个点不选,那么就需要与这个点相连的所有点都选,而守点则只需要与这个点相连的任一点选择即可。
Solution
设 fu,0/1f_{u,0/1}fu,0/1 表示 uuu 节点选 / 不选。
如果 uuu 不选,那么他的所有儿子必选,因为需要满足看守所有的边,fu,0=∑v∈son(u)fv,1f_{u,0}=\sum_{v\in \text{son}(u)} f_{v,1}fu,0=∑v∈son(u)fv,1;
如果 uuu 选,那么他的儿子选不选并不重要,fu,0=∑v∈son(u)min{fv,0,fv,1}f_{u,0}=\sum_{v\in \text{son}(u)}\min\{f_{v,0}, f_{v,1}\}fu,0=∑v∈son(u)min{fv,0,fv,1}。
Core Code
1
2
3
4
5
6
7
8
9
void dfs(int u,int fa) {
    f[u][1]=1;
    for(auto v:e[u]) {
        if(v==fa) continue;
        dfs(v,u);
        f[u][0]+=f[v][1];
        f[u][1]+=min(f[v][0],f[v][1]);
    }
}
P3478 [POI2008] STA-Station
Link
给出一个 nnn 个点的树,找出一个点,使以这个点为根的树所有点的深度之和最大
tag: 树形 DP, 换根 DP
Solution
换根板子。
首先考虑如何暴力,显然是以每个点为根都跑一边 dfs,时间复杂度 O(n2)\mathcal O(n^2)O(n2)。
一般换根 DP 题的处理思路是:
先指定一个根节点,一次 dfs 自下而上,用子节点的值更新父节点;
从上一步的根出发,dfs 转移父节点对子节点的贡献。
设 fuf_{u}fu 为以 uuu 为根的深度和。运用人类智慧可以注意到 uuu 节点的值可以通过其父亲转移。如下图,我们已经处理好了 222 号节点的答案,那么发现以 444 为根的时,444 的子树(包括自己)的相对深度都减少了 111,而 444 上面的三个点深度都增加了 111。
可以得到转移方程:
fv=fu−sizv×1+(n−sizv)×1=fu+n−2×sizvf_{v}=f_{u}-\text{siz}_{v}\times 1+(n-\text{siz}_{v})\times 1=f_u+n-2\times \text{siz}_vfv=fu−sizv×1+(n−sizv)×1=fu+n−2×sizv
其中 sizv×1\text{siz}_v\times 1sizv×1 表示 444 的子树,n−sizvn-\text{siz}_vn−sizv 表示 444 上面的点。
Core Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void dfs1(int u,int fa) {
    g.siz[u]=1;
    for(auto v:e[u]) {
        if(v==fa) continue;
        g.dep[v]=g.dep[u]+1;
        dfs1(v,u);
        g.siz[u]+=g.siz[v];
    }
}
void dfs2(int u,int fa) {
    for(auto v:e[u]) {
        if(v==fa) continue;
        f[v]=f[u]+n-2ll*g.siz[v];
        dfs2(v,u);
    }
}
int main() {
    dfs1(1,0);
    for(int i=2;i<=n;i++) {
        f[1]+=g.dep[i];
    }
    dfs2(1,0);
    ll ans=-INF,maxn=-INF;
    for(int i=1;i<=n;i++) {
        if(maxn<f[i]) {
            maxn=f[i];
            ans=i;
        }
    }
    cout<<ans<<endl;
    return 0;
}
P3047 [USACO12FEB] Nearby Cows G
Link
给你一棵 nnn 个点的树,点带权,对于每个节点求出距离它不超过 kkk 的所有节点权值和 mim_imi。
tag: 换根 DP
Solution
设 fu,df_{u,d}fu,d 为与 uuu 节点距离小于 kkk 的权值和。
两边 dfs,第一遍向上转移,计算子树对当前节点的贡献。注意到与 uuu 节点距离小于 kkk 的节点数可以由它的左右子树转移而来,等价于其与所有子节点距离小于 k−1k-1k−1 的节点数之和,得到 fu,d=∑v∈son(u)fv,d−1f_{u,d}=\sum_{v\in \text{son}(u)} f_{v,d-1}fu,d=∑v∈son(u)fv,d−1。
第二遍时计算父亲对当前节点的贡献。同样可以由一的思路 fv,d=fu,d−1(v∈son(u))f_{v,d}=f_{u,d-1}(v\in \text{son}(u))fv,d=fu,d−1(v∈son(u)),但是注意到这样会重复计算子树中一部分的节点,因为到 uuu 距离为 ddd 的点包含到 vvv 距离为 d−1d-1d−1 的点,如下图的 f2,2f_{2,2}f2,2 就包含了 f3,1f_{3,1}f3,1 的贡献,所以我们简单容斥一下减去 fv,d−2f_{v,d-2}fv,d−2 的部分。
注意在进行第二遍 dfs 时,要注意循环顺序,防止容斥后的 fff 影响未更新的。
Core Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void dfs1(int u, int fa) {
    for (auto v : e[u]) {
        if (v == fa) continue;
        dfs1(v, u);
        for (int d = 1; d <= k; d++) {
            f[u][d] += f[v][d-1];
        }
    }
}
void dfs2(int u, int fa) {
    for (auto v : e[u]) {
        if (v == fa) continue;
        for (int d = 1; d <= k; d++) {
            f[v][d] += f[u][d-1];
        }
        for (int d = k; d >= 2; d--) {
            f[v][d] -= f[v][d-2];
        }
        dfs2(v, u);
    }
}
CF474E Pillars
Link
给定序列长度为 nnn 的序列 aaa,和 ddd。
找出一个最长的 aaa 的子序列 bbb(设其长度为 mmm),使得对于任意的 1≤i<m1\le i\lt m1≤i<m,有 ∣bi+1−bi∣≥d|b_{i+1}-b_i|\ge d∣bi+1−bi∣≥d。
输出 mmm 和序列 bbb 在序列 aaa 中每个数的下标。
tag: 动态开点线段树, 线段树优化 DP
Solution
设 fif_ifi 表示前 iii 位的最大长度,可以得到一个显然的转移方程
fi=max∣bi−bj∣≥d{fj}+1=max1≤j≤ai−d or ai+d≤j≤n{fj}+1\begin{aligned}f_i &= \max_{\lvert b_i - b_j\rvert \geq d}\{f_j\} + 1\\&=\max_{1\leq j\leq a_i - d\ \text{or}\ a_i + d\leq j\leq n}\{f_j\} + 1\end{aligned}fi=∣bi−bj∣≥dmax{fj}+1=1≤j≤ai−d or ai+d≤j≤nmax{fj}+1
考虑线段树优化掉两部分区间最值。使用权值线段树,每次计算完 iii 后把 fif_ifi 放到对应的 aia_iai 的位置即可。
数据范围过大,需动态开点或离散化。
Core Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
int n, d;
int tot = 0;
int rt;
struct pr {
    int val, id;
    bool operator < (pr const& x) {
        return val < x.val;
    }
}a[N], f[N], pre[N];
pr max(pr x, pr y) {
    if (x < y) return y;
    else return x;
}
struct node {
    pr val;
    int l, r;
}t[40 * N];
struct tree {
    void pushup(int p) {
        t[p].val = max(t[t[p].l].val, t[t[p].r].val);
    }
    void modify(int &p, int l, int r, int x, pr w) {
        if (!p) p = ++tot;
        if (l == r) {
            if (t[p].val < w) t[p].val = w;
            return;
        }
        int mid = (l + r) >> 1;
        if (x <= mid) modify(t[p].l, l, mid, x, w);
        else modify(t[p].r, mid + 1, r, x, w);
        pushup(p);
    }
    pr query(int p, int l, int r, int x, int y) {
        if (!p || y < x) return {0, 0};
        if (x <= l && r <= y) {
            return t[p].val;
        }
        int mid = (l + r) >> 1;
        pr res = {-INF, 0};
        if (x <= mid) res = max(res, query(t[p].l, l, mid, x, y));
        if (y >= mid + 1) res = max(res, query(t[p].r, mid + 1, r, x, y));
        return res;
    }
}sgt;
signed main() {
    n = read(); d = read();
    for (int i = 1; i <= n; i++) {
        a[i].val = read();
        a[i].id = i;
    }
    pr maxn = {0, 0};
    for (int i = 1; i <= n; i++) {
        f[i] = max(sgt.query(rt, 1, MAXN, 1, a[i].val - d),
                   sgt.query(rt, 1, MAXN, a[i].val + d, MAXN));
        bug(f[i].id);
        f[i].val += 1;
        maxn = max(maxn, {f[i].val, i});
        sgt.modify(rt, 1, MAXN, a[i].val, {f[i].val, i});
    }
    cout << maxn.val << endl;
    stack<int> st;
    for (int i = maxn.id; i >= 1; i = f[i].id) {
        bug(f[i].id);
        st.push(i);
    }
    while (!st.empty()) {
        cout << st.top() << " ";
        st.pop();
    }
    return 0;
}
CF708C Centroids
Link
给定一颗树,你有一次将树改造的机会,改造的意思是删去一条边,再加入一条边,保证改造后还是一棵树。
请问有多少点可以通过改造,成为这颗树的重心?(如果以某个点为根,每个子树的大小都不大于 n2\dfrac{n}{2}2n,则称某个点为重心)
tag: 树形 DP, 换根 DP
Solution
根据重心的定义,如果一个点 uuu 不是重心,那么它的子树中一定有且仅有一个的 siz>⌊n2⌋siz > \lfloor\frac{n}{2}\rfloorsiz>⌊2n⌋。如果想要把 uuu 变成重心,那么就需要在这个子树中找到一个更小的子树,把它接到 uuu 上,要解决的是是否存在一个这样的子树可以满足条件。
如果先将一个不需要操作即可满足条件的节点钦定为根,那么这棵树的所有子树都满足 siz≤⌊n2⌋siz\leq \lfloor\frac{n}{2}\rfloorsiz≤⌊2n⌋。
所以,若在这棵新树上进行 DP,对于任何一个节点 uuu,如果它不是重心,只能因为除了它的子树以外的点数不满足要求,即 n−sizun - siz_un−sizu。我们记录一个 gug_ugu 表示除了 uuu 子树以外的,不超过 ⌊n2⌋\lfloor\frac{n}{2}\rfloor⌊2n⌋ 的最大子树大小,这个子树就是用来切下来挂到 uuu 上的。
那么,只需要判断除了上面这两部分一定合法的,剩下的那些点是否合法,即 n−sizu−gu≤⌊n2⌋n - siz_u - g_u \leq \lfloor\frac{n}{2}\rfloorn−sizu−gu≤⌊2n⌋ 是否成立。
 
也就是说,问题转化为求 gug_ugu。
考虑自上而下转移,gug_ugu 可能来自于(faufa_ufau 的子树)/(gfaug_{fa_u}gfau)/(除了子树以外所有)。由于第一种情况,还要记录一个 fuf_ufu 表示以 uuu 为根的子树中,不超过 ⌊n2⌋\lfloor\frac{n}{2}\rfloor⌊2n⌋ 的最大子树大小。我们还可以发现,在转移 gug_ugu 时,可能 ffauf_{fa_u}ffau 就表示 uuu 的子树,为了解决这种情况,需要记录最大值 fu,0f_{u, 0}fu,0 和次大值 fu,1f_{u, 1}fu,1。
显然 fu,0f_{u, 0}fu,0 需要自下而上转移:
fu,0={fv,0,sizv>⌊n2⌋sizu,sizv≤⌊n2⌋f_{u, 0}=\begin{cases}f_{v, 0}, &\text{siz}_v >\lfloor\frac{n}{2}\rfloor\\\text{siz}_u, &\text{siz}_v \leq\lfloor\frac{n}{2}\rfloor\end{cases}fu,0={fv,0,sizu,sizv>⌊2n⌋sizv≤⌊2n⌋
可以按下面的 trick 维护次大值,为了表示方便,用 xxx 表示需要被更新的数:
{fu,1=fu,0,fu,0=x,fu,0<xfu,1=x,fu,0≤x<fu,1\begin{cases}f_{u, 1} = f_{u, 0}, f_{u, 0} = x, &f_{u, 0} < x\\f_{u, 1} = x, &f_{u, 0}\leq x < f_{u, 1}\end{cases}{fu,1=fu,0,fu,0=x,fu,1=x,fu,0<xfu,0≤x<fu,1
然后即可自上而下转移 gug_ugu:
gv=max{fu,1,fu,0=sizvfu,0,fu,0≠sizvgun−sizv,n−sizv≤⌊n2⌋g_v=\max\begin{cases}f_{u, 1}, &f_{u, 0} = \text{siz}_v\\f_{u, 0}, &f_{u, 0} \neq \text{siz}_v\\g_u\\n - \text{siz}_v, &n - \text{siz}_v \leq \lfloor\frac{n}{2}\rfloor\end{cases}gv=max⎩
⎨
⎧fu,1,fu,0,gun−sizv,fu,0=sizvfu,0=sizvn−sizv≤⌊2n⌋
Core Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
void findrt(int u, int fa) {
    for (auto v : e[u]) {
        if (v == fa) continue;
        findrt(v, u);
        siz[u] += siz[v];
        if (siz[v] > n / 2) return;
    }
    if (n - siz[u] > n / 2) return;
    root = u;
}
void dfs1(int u, int fa) {
    for (auto v : e[u]) {
        if (v == fa) continue;
        dfs1(v, u);
        siz[u] += siz[v];
        if (siz[v] > n / 2) {
            if (f[v][0] > f[u][0]) {
                f[u][1] = f[u][0];
                f[u][0] = f[v][0];
            } else if (f[v][0] > f[u][1]) {
                f[u][1] = f[v][0];
            }
        } else {
            if (siz[v] > f[u][0]) {
                f[u][1] = f[u][0];
                f[u][0] = siz[v];
            } else if (siz[v] > f[u][1]) {
                f[u][1] = siz[v];
            }
        }
    }
}
void dfs2(int u, int fa) {
    for (auto v : e[u]) {
        if (v == fa) continue;
        g[v] = max(g[v], g[u]);
        if (n - siz[v] <= n / 2) {
            g[v] = max(g[v], n - siz[v]);
        }
        if (f[u][0] == siz[v]) {
            g[v] = max(g[v], f[u][1]);
        } else {
            g[v] = max(g[v], f[u][0]);
        }
        dfs2(v, u);
    }
    if (n - siz[u] - g[u] <= n / 2 || u == root) ans[u] = 1;
}
int main() {
    n = read();
    for (int i = 1; i < n; i++) {
        int u = read(), v = read();
        e[u].push_back(v);
        e[v].push_back(u);
    }
    for (int i = 1; i <= n; i++) siz[i] = 1;
    findrt(1, 0);
    for (int i = 1; i <= n; i++) siz[i] = 1;
    dfs1(root, 0);
    dfs2(root, 0);
    for (int i = 1; i <= n; i++) {
        cout << ans[i] << " ";
    }
    el;
}
AT_joi2015ho_b Cake 2
Link
nnn 个数 aia_iai 围成环,AAA 和 BBB 两人轮流取数,AAA 先取,两人都将采取最优策略,求最大化 AAA 取到的数的和。
tag: 区间 DP, 环
Solution
设 fi,jf_{i, j}fi,j 表示剩余的蛋糕区间为 [i,j][i, j][i,j] 时最大的答案,把 aaa 断环为链。
考虑如何转移,由于两人轮流取,需要分类讨论:
当前轮 AAA 取,剩余区间为 [l,r][l, r][l,r],显然一定满足的前提条件是 remain≡n(mod2)\text{remain}\equiv n \pmod 2remain≡n(mod2),其中 remain\text{remain}remain 表示的是剩余蛋糕区间长度(简单手玩即可得证)。此时,AAA 可以取 ala_lal 或 ara_rar,此时 fl,r=max{fl,r−1+ar,fl+1,r+al}f_{l, r}=\max\{f_{l, r-1}+a_r, f_{l+1, r}+a_l\}fl,r=max{fl,r−1+ar,fl+1,r+al}。
当前轮 BBB 取,变量定义同上,前提条件为 remain\text{remain}remain 和 nnn 奇偶性不同。此时 BBB 一定取 max{al,ar}\max\{a_l, a_r\}max{al,ar},那么留给 AAA 的就是其中较小的一个。
所以有如下方程:
len≔l−r+1fl,r={max{fl,r−1+ar,fl+1,r+al},len≡n(mod2)fl,r−1,al<ar and len≢n(mod2)fl+1,r,al>ar and len≢n(mod2)\text{len}\coloneqq l-r+1\\f_{l, r}=\begin{cases}\max\{f_{l, r-1}+a_r, f_{l+1, r}+a_l\}, &\text{len}\equiv n \pmod 2\\f_{l, r-1}, &a_l<a_r\ \text{and}\ \text{len}\not\equiv n \pmod 2\\f_{l+1, r}, &a_l>a_r\ \text{and}\ \text{len}\not\equiv n \pmod 2\end{cases}len:=l−r+1fl,r=⎩
⎨
⎧max{fl,r−1+ar,fl+1,r+al},fl,r−1,fl+1,r,len≡n(mod2)al<ar and len≡n(mod2)al>ar and len≡n(mod2)
最后考虑边界,如果最后一轮是 AAA 取的,也就是 nnn 为奇数时,对于任意位置 iii,贡献都为 aia_iai;反之由 BBB 取,那么贡献为 000。
Core Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
n = read();
for (int i = 1; i <= n; i++) {
    a[i] = read();
    a[i + n] = a[i];
}
a[0] = a[n];
if (n & 1) {
    for (int i = 1; i <= n * 2; i++) {
        f[i][i] = a[i];
    }
}
for (int len = 2; len <= n; len++) { // len := remain cakes
    for (int l = 1; l + len - 1 <= n * 2; l++) {
        int r = l + len - 1;
        if ((len & 1) == (n & 1)) { // JOI
            f[l][r] = max({f[l][r], f[l + 1][r] + a[l], f[l][r - 1] + a[r]});
        } else { // IOI
            if (a[l] < a[r]) f[l][r] = max(f[l][r], f[l][r - 1]);
            else f[l][r] = max(f[l][r], f[l + 1][r]);
        }
    }
}
int ans = 0;
for (int i = 1;i <= n; i++) {
    ans = max(ans, f[i][i + n - 1]);
}
cout << ans << endl;
P9759 [COCI2022-2023#3] Bomboni
Link
    
      
        
▶题意      
      
        
          
有 n×nn\times nn×n 的方格。目前在左上角。通过向右或向下移动,要前往右下角。目前所在的格子没有障碍。
在每个格子中写了一个数字表示此地为糖果或障碍。Iva 会吃掉所有经过的糖果(包括起点和终点的糖果)并且将糖果对应的数字相乘。Iva 知道她自己最喜欢的数字是 kkk,所以她希望这个乘积结果能被 kkk 整除。她想知道一共有多少条这样的路径。由于答案可能很大,她只想知道答案模 998244353998244353998244353 的结果。
        
      
    
tag: trick
Solution
考虑从因数角度分析,发现只要一条路径上覆盖了 kkk 的因数,这条路径就是合法的。
设 fi,j,tf_{i, j, t}fi,j,t 为前 (i,j)(i, j)(i,j) 位置时 kkk 的因数剩余 ttt 的路径数量。那么每次转移,从上一步去掉 (i,j)(i, j)(i,j) 能贡献的因数,作为这次的答案。
最终答案即为 fn,n,1f_{n, n, 1}fn,n,1。
Core Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
f[1][1][k / __gcd(a[1][1], k)] = 1;
for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= n; j++) {
        if (i - 1 >= 1 && a[i][j] != -1) {
            for (auto x : f[i - 1][j]) {
                int nw = x.first / __gcd(x.first, a[i][j]);
                f[i][j][nw] = (f[i][j][nw] + x.second + mod) % mod;
            }
        }
        if (j - 1 >= 1 && a[i][j] != -1) {
            for (auto x : f[i][j - 1]) {
                int nw = x.first / __gcd(x.first, a[i][j]);
                f[i][j][nw] = (f[i][j][nw] + x.second + mod) % mod;
            }
        }
    }
}
CF360B Levko and Array
Link
给定一个数列 aaa,可以对其中的元素做至多 kkk 次修改,每次修改可以将数列中的一个数改成另一个。
求经过修改后,maxi=2n∣ai−ai−1∣\max\limits^n_{i=2}\left|a_i-a_{i-1}\right|i=2maxn∣ai−ai−1∣ 的最小值。
tag: 二分, trick
Solution
注意到答案具有单调性,二分答案 xxx。
考虑 check() 函数的写法,首先可以将题目中要求的最多 kkk 次修改转化为至少保留 n−kn-kn−k 个数。
不妨进行 DP,设 fif_ifi 表示前 iii 个数中在保留 aia_iai 的情况下最多能保留的数量。两个数可以被保留,当且仅当 ∣aj−ai∣≤(j−i)×x\lvert a_j-a_i\rvert\leq (j-i)\times x∣aj−ai∣≤(j−i)×x。这个比较好感性理解,两个位置 (i,j)(i,j)(i,j) 之间有 j−i+1j-i+1j−i+1 个数,也就意味着有 j−ij-ij−i 个间隔,而这 j−ij-ij−i 个间隔中每个间隔最大的符合条件的差是 xxx。
那么式子就很显然了:
fi=max1≤j<i{fj+1∣∣aj−ai∣≤(j−i)×x}f_i=\max_{1\leq j<i}\{f_j+1 \big| \lvert a_j-a_i\rvert\leq (j-i)\times x\}fi=1≤j<imax{fj+1
∣aj−ai∣≤(j−i)×x}
Core Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const int N = 2e3 + 10;
const int MAXN = 2e9;
int n, k;
int a[N];
int f[N];
bool check(int x) {
    for (int i = 1; i <= n; i++) f[i] = 1;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j < i; j++) {
            if (abs(a[i] - a[j]) <= (i - j) * x) {
                f[i] = max(f[i], f[j] + 1);
            }
        }
        if (f[i] >= n - k) return 1;
    }
    return 0;
}
signed main() {
    n = read(); k = read();
    for (int i = 1; i <= n; i++) {
        a[i] = read();
    }
    int l = 0, r = MAXN;
    while (l < r) {
        int mid = (l + r) >> 1;
        if (check(mid)) r = mid;
        else l = mid + 1;
    }
    cout << l << endl;
    return 0;
}
P4158 [SCOI2009] 粉刷匠
Link
有 NNN 条木板需要被粉刷。每条木板被分为 MMM 个格子。 每个格子要被刷成 0/10/10/1。每次粉刷只能选择一条木板上一段连续的格子,然后涂上一种颜色。每个格子最多只能被粉刷一次。只能粉刷 TTT 次,他最多能正确粉刷多少格子?
tag: 背包
Solution
设 fi,jf_{i,j}fi,j 表示前 iii 行刷 jjj 次最多正确的格子数,发现这是一个背包,容易得到下面的转移:
fi,j=max1≤k≤j{fi−1,j−k+gn,k}f_{i,j}=\max_{1\leq k\leq j}\{f_{i-1,j-k}+g_{n, k}\}fi,j=1≤k≤jmax{fi−1,j−k+gn,k}
其中,gi,jg_{i, j}gi,j 表示当前行前 iii 格刷 jjj 次最多正确的格子数。它可以在每次枚举行的时候更新,有转移:
gi,j=max0≤l<i{gl,j−1+hl+1,i}g_{i,j}=\max_{0\leq l<i}\{g_{l, j-1}+h_{l+1,i}\}gi,j=0≤l<imax{gl,j−1+hl+1,i}
其中,hl,rh_{l, r}hl,r 表示当前行刷 [l,r][l, r][l,r] 时刷一次最多刷对的格子数,也就是区间中 000 和 111 数量的最大值。
只需要在背包的转移前维护一下 ggg 和 hhh 即可。
Core Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
for (int i = 1; i <= n; i++) {
    memset(g, 0, sizeof(g));
    memset(maxn, 0, sizeof(maxn));
    for (int l = 1; l <= m; l++) {
        for (int r = l; r <= m; r++) {
            int cnt0 = 0, cnt1 = 0;
            for (int p = l; p <= r; p++) {
                if (s[i][p] == '0') cnt0++;
                else cnt1++;
            }
            maxn[l][r] = max(cnt0, cnt1);
        }
    }
    for (int j = 1; j <= m; j++) {
        for (int k = 1; k <= t; k++) {
            for (int l = 0; l < j; l++) {
                g[j][k] = max(g[j][k], g[l][k - 1] + maxn[l + 1][j]);
            }
        }
    }
    for (int j = t; j >= 1; j--) {
        for (int k = 1; k <= j; k++) {
            f[j] = max(f[j], f[j - k] + g[m][k]);
        }
    }
}
P1156 垃圾陷阱
Link
卡门掉进深度为 DDD 英尺的垃圾井。她需要堆满垃圾,使垃圾高度 ≥D\geq D≥D 才能逃出。每个垃圾有扔下时间 ttt、堆放高度 hhh 和食用后维持生命时间 lll。卡门起始有 101010 小时的能量,101010 小时内若不进食将饿死。若体力为 0,吃垃圾或逃出井不会饿死。求卡门最早能逃出井的时间。
tag: 背包
Solution
看到两种选择,想到背包。用 fi,jf_{i, j}fi,j 表示前 iii 个物品堆放高度为 jjj 时的最大体力。
本题与普通 0/10/10/1 背包不同的地方在于,本题有两种情况的成立条件和贡献不同,需要分类讨论:
如果选择吃掉,那么 fi,j=fi−1,j+lif_{i,j}=f_{i-1,j}+l_ifi,j=fi−1,j+li。需要满足转移前一步剩余的体力能够等到当前垃圾扔下,即 fi−1,j≥tif_{i-1,j}\geq t_ifi−1,j≥ti;
如果选择不吃,那么 fi,j=fi−1,j−hif_{i,j}=f_{i-1,j-h_i}fi,j=fi−1,j−hi。需要满足转移前一步剩余的体力能够等到当前垃圾仍下,即 fi−1,j−hi≥ti (j≥hi)f_{i-1,j-h_i}\geq t_i\ (j\geq h_i)fi−1,j−hi≥ti (j≥hi)。
需要特判 hi>dh_i>dhi>d 的情况。
Core Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
memset(f, -INF, sizeof(f));
f[0] = 10;
d = read(), g = read();
for (int i = 1; i <= g; i++) {
    a[i].t = read(), a[i].f = read(), a[i].h = read();
    maxn = max(maxn, a[i].h);
}
sort(a + 1, a + g + 1);
for (int i = 1; i <= g; i++) {
    for (int j = max(d, maxn); j >= 0; j--) {
        if (f[j] >= a[i].t) {
            f[j] = max(f[j], f[j] + a[i].f);
        }
        if (j >= a[i].h && f[j - a[i].h] >= a[i].t) {
            f[j] = max(f[j], f[j - a[i].h]);
        }
    }
    if (d > maxn) {
        if (f[d] >= 0) {
            cout << a[i].t << endl;
            return 0;
        }
    } else {
        for (int j = d; j <= maxn; j++) {
            if (f[j] >= 0) {
                cout << a[i].t << endl;
                return 0;
            }
        }
    }
}
P1772 [ZJOI2006] 物流运输
Link
物流公司要把一批货物从码头 111 运到码头 nnn。由于货物量比较大,需要 ttt 天才能运完。货物运输过程中一般要转停好几个码头。
每日会从 111 港口发货;
每日有可能会有港口封锁,封锁处不可走,需要换航线并加上换航线的成本。
tag: 最短路
Solution
首先想到可以对于每一天跑一遍最短路来预处理。
考虑 DP,设 fif_{i}fi 表示前 iii 天的最小成本。对于新的一天,有继续沿用之前的路线和走新的路线两种情况。所以设 minni,j\text{minn}_{i, j}minni,j 表示 iii 到 jjj 天走同一条路线的最短路径,显然可以对于每一个 i,ji, ji,j,跑一遍最短路预处理。此时有转移方程:
fi=max1≤i<j{fj+minnj+1,i×(i−j)+k}f_{i}=\max_{1\leq i<j}\{f_{j}+\text{minn}_{j+1, i}\times (i-j)+k\}fi=1≤i<jmax{fj+minnj+1,i×(i−j)+k}
另外还有前 iii 天都走同一条路的情况,fi=minn1,i×if_{i}=\text{minn}_{1,i}\times ifi=minn1,i×i。
Core Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
for (int i = 1; i <= d; i++) {
    p = read(), a = read(), b = read();
    for (int j = a; j <= b; j++) {
        block[p][j] = 1;
    }
}
for (int i = 1; i <= t; i++) {
    for (int j = i; j <= t; j++) {
        mp.reset();
        for (int p = 1; p <= n; p++) {
            for (int l = i; l <= j; l++) {
                if (block[p][l]) {
                    mp[p] = 1;
                    break;
                }
            }
        }
        minn[i][j] = dij();
    }
}
for (int i = 1; i <= t; i++) {
    f[i] = minn[1][i] * i;
}
for (int i = 1; i <= t; i++) {
    for (int j = 1; j < i; j++) {
        f[i] = min(f[i], f[j] + minn[j + 1][i] * (i - j) + k);
    }
}
P3174 [HAOI2009] 毛毛虫
Link
给定一颗 nnn 个节点的多叉树,找出包含分支最多的一条链,输出节点数。其中分支只计算与链直连的节点。
tag: 树形 DP, 树的直径
Solution
考虑从当前节点能控制的下一层节点入手,如果当前节点 uuu 在链中,那么它所能控制的下一层节点数即为 degu−1\text{deg}_u-1degu−1,其中 degu\text{deg}_udegu 表示 uuu 的度。也就是说,可以按树的直径的思路,两遍 dfs,在递归的过程中统计答案。
注意需要考虑根节点没有父亲所带来的影响,和 n=1n=1n=1 的特殊情况。
Core Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void dfs(int u, int fa, int sum) {
    if (sum > maxd) {
        maxd = sum;
        maxpos = u;
    }
    for (auto v : e[u]) {
        if (v == fa) continue;
        dfs(v, u, sum + deg[u] - 1);
    }
}
int main() {
    n = read(), m = read();
    if (n == 1) {
        cout << 1 << endl;
        return 0;
    }
    for (int i = 1; i <= m; i++) {
        int u = read(), v = read();
        e[u].push_back(v);
        e[v].push_back(u);
        deg[u]++, deg[v]++;
    }
    dfs(1, 0, deg[1] - 1);
    maxd = 0;
    dfs(maxpos, 0, deg[maxpos] - 1);
    cout << maxd + 2 << '\n';
    return 0;
}
P1131 [ZJOI2007] 时态同步
Link
给定一颗 nnn 个节点带边权的树,需要对于每颗子树调整根到叶子节点的路径
长度相等。每次操作可以对任意边权 +1+1+1。求最小操作次数。
tag: 树形 DP
Solution
要使操作次数最小,对于每个子树,就要操作根节点下的第一条边。所以从下向上递归更新答案,对于每个节点 uuu 记录一个以 uuu 为根的子树到叶节点的最大距离。遍历个儿子,累加缺少的距离。
Core Code
1
2
3
4
5
6
7
8
9
10
11
12
13
void dfs(int u, int fa) {
    for (auto x : e[u]) {
        int v = x.v, w = x.w;
        if (v == fa) continue;
        dfs(v, u);
        f[u] = max(f[u], f[v] + w);
    }
    for (auto x : e[u]) {
        int v = x.v, w = x.w;
        if (v == fa) continue;
        ans += f[u] - (f[v] + w);
    }
}
CF1324F Maximum White Subtree
Link
给定一棵 nnn 个节点无根树,每个节点 uuu 有一个颜色 aua_uau,若 aua_uau 为 000 则 uuu 是黑点,若 aua_uau 为 111 则 uuu 是白点。
对于每个节点 uuu,选出一个包含 uuu 的连通子图,设子图中白点个数为 cnt1cnt_1cnt1,黑点个数为 cnt2cnt_2cnt2,请最大化 cnt1−cnt2cnt_1 - cnt_2cnt1−cnt2。并输出这个值。
tag: 换根 DP
Solution
记 coloru\text{color}_ucoloru 表示 uuu 节点的颜色,黑白为 −1/1-1/1−1/1。
设 fuf_ufu 表示以 111 为根时 uuu 子树的贡献。自下向上转移,容易得到
fu=coloru+∑v∈son(u)max{0,fv}f_u=\text{color}_u+\sum_{v\in \text{son}(u)}\max\{0, f_{v}\}fu=coloru+v∈son(u)∑max{0,fv}
考虑换根,设 gug_ugu 表示以 uuu 为根时的答案,答案分为两部分:以 uuu 为根的子树的贡献和剩余部分的贡献。第一部分显然为 fuf_ufu,第二部分可以通过父亲的答案转移,也就是 gfau−fug_{fa_u}-f_ugfau−fu。通过前面的式子可以知道,如果 fu<0f_u<0fu<0,fuf_ufu 不会被记录到 ffauf_{fa_u}ffau 中,也就是说,gfaug_{fa_u}gfau 的答案不包含 fuf_ufu。所以最终的式子
gu=fu+max{0,gfau−max{0,fu}}g_u=f_u+\max\{0, g_{fa_u}-\max\{0, f_u\}\}gu=fu+max{0,gfau−max{0,fu}}
Core Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void dfs1(int u, int fa) {
    f[u] = a[u];
    for (auto v : e[u]) {
        if (v == fa) continue;
        dfs1(v, u);
        f[u] += max(0, f[v]);
    }
}
void dfs2(int u, int fa) {
    for (auto v : e[u]) {
        if (v == fa) continue;
        g[v] = f[v] + max(0, g[u] - max(0, f[v]));
        dfs2(v, u);
    }
}
g[1]=f[1]
CF607B Zuma
Link
给定一个序列,每次可删除任意长度的回文子串,最小化将序列清空的删除次数。
tag: 区间 DP
Solution
设 fl,rf_{l,r}fl,r 表示清空 [l,r][l,r][l,r] 的最小操作次数。
最朴素的情况是枚举中间点,分成两部分来求,fl,r=min{fl,i+fi+1,r}(i∈[l,r))f_{l,r}=\operatorname{min}\{f_{l,i}+f_{i+1,r}\}(i\in[l,r))fl,r=min{fl,i+fi+1,r}(i∈[l,r))。
当 al=ara_l=a_ral=ar 时, 有另外的情况:
[l+1,r−1][l+1,r-1][l+1,r−1] 回文串,此时加上两端相等的值还是回文串;
[l+1,r−1][l+1, r-1][l+1,r−1] 不是回文串,此时内部随便删,删到内部只剩一个的时候,又可以与两端构成回文串。
所以 fl,r=min{fl,r,fl+1,r−1}(al=ar)f_{l,r}=\operatorname{min}\{f_{l,r},f_{l+1,r-1}\}(a_l=a_r)fl,r=min{fl,r,fl+1,r−1}(al=ar)。
两情况取最小值即可。
Core Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
memset(f, INF, sizeof(f));
for (int i = 1; i <= n; i++) {
    a[i] = read();
    f[i][i] = 1;
}
for (int len = 2; len <= n; len++) {
    for (int l = 1; l + len - 1 <= n; l++) {
        int r = l + len - 1;
        if (a[l] == a[r]) {
            if (len == 2) {
                f[l][r] = 1;
            } else {
                f[l][r] = f[l + 1][r - 1];
            }
        }
        for (int i = l; i < r; i++) {
            f[l][r] = min(f[l][r], f[l][i] + f[i + 1][r]);
        }
    }
}