AT_dp_[ ]
题单跳转
1. 无聊的统计
难度统计
| 难度 | 数量 | 题目 |
|---|---|---|
| 橙 | 4 | A,B,D,H |
| 黄 | 7 | C,E,F,G,K,L,N |
| 绿 | 6 | I,J,M,P,Q,R |
| 蓝 | 7 | O,S,T,U,W,X,Y |
| 紫 | 2 | V,Z |
该博客的 Markdown 源码共有 \(\approx 1.03\times 10^4\) 字符(截至最后一次编辑),673 行。
2. 正文
橙 - A,B,D,H
A
设 \(f_i\) 表示到第 \(i\) 个石头的费用。
则易得转移方程 \(f_i=\min\!\left\{f_{i-1}+|h_{i-1}-h_i|,f_{i-2}+|h_{i-2}-h_i|\right\}\)。
初始化 \(f_0=\inf\)。
B
与 A 类似,但不同的是,我们需要枚举前 \(k\) 个石头。
此时变为 \(f_i=\min\limits_{1\le j\le k}\!\left\{f_{i-j}+|h_{i-j}-h_i|\right\}\)。
注意不要访问到负数下标。
D
01 背包板子。
设 \(f_{i,j}\) 表示只有前 \(i\) 个物品且容量为 \(j\) 时的最大价值。
不选时,\(f_{i,j}=f_{i-1,j}\);
选时,\(f_{i,j}=f_{i-1,j-w_i}+v_i\)。
两者取 \(\min\) 即可。
H
过河卒加强(多个障碍)+弱化(不用向八个方向扩散)版。
设 \(f_{i,j}\) 为走到 \((i,j)\) 时的方案数。
则 \(f_{i,j}=\begin{cases}0 & (i,j)\,\text{为障碍} \\ f_{i-1,j}+f_{i,j-1} & (i,j)\,\text{不是障碍}\end{cases}\)
黄 - C,E,F,G,K,L,N
C
这不是橙题吗。
设 \(f_{i,j}\) 为前 \(i\) 天中最后一天选择 \(j\) 活动时的快乐值。
则 \(f_{i,1}=f_{i-1,2}+f_{i-1,3}\),其他同理。
答案取 \(\max\left\{f_{n,1},f_{n,2},f_{n,3}\right\}\)。
E
01 背包,但是 \(W\le 10^9\),用 \(O(nW)\) 会爆。
注意到 \(v_i\le 10^3\),我们可以反向思考,设 \(f_{i,j}\) 表示只有前 \(i\) 个物品且 总价值 为 \(j\) 时的 最小容量。
不选时 \(f_{i,j}=f_{i-1,j}\),选时 \(f_{i,j}=f_{i-1,j-v_i}+w_i\)。
最后选总价值最大且容量 \(\le W\) 的值。
时间复杂度 \(O(n^2V)\),其中 \(V=\max v_i\)。
F
设 \(f_{i,j}\) 为 \(a\) 的前 \(i\) 个字符与 \(b\) 的前 \(j\) 个字符的 LCS 长度。
则 \(f_{i,j}=\begin{cases}0 & i\le0\text{ or } j\le0\\f_{i-1,j-1}+1 & a_i=b_j \\ \max\{f_{i,j-1},f_{i-1,j}\} & a_i\not=b_j\end{cases}\)
但题目要求输出的是答案而非长度。
我们可以记录一个数组 pre[i][j],表示 \(f_{i,j}\) 在递推时用的是哪个位置的数。
在输出时,我们就可以后序遍历,从右下角通过 pre 数组的指向走到左上角,并在 \(a_i=b_j\) 时及时输出。
code
// 版权归 @CuSO4_ 所有
// 未经许可,不许转载
#include<bits/stdc++.h>
using namespace std;
int n,m;
string s,t;
int dp[3140][3140],pre[3140][3140];
void opt(int p,int q){
if(p==0&&q==0){
return;
}
if(pre[p][q]==1){
opt(p-1,q-1);
cout<<s[p];
}
if(pre[p][q]==2) opt(p,q-1);
if(pre[p][q]==3) opt(p-1,q);
}
int main(){
cin>>s>>t;
n=s.length(); m=t.length(); s="#"+s; t="$"+t;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(s[i]==t[j]){
dp[i][j]=dp[i-1][j-1]+1;
pre[i][j]=1;
}else{
if(dp[i-1][j]<dp[i][j-1]){
dp[i][j]=dp[i][j-1];
pre[i][j]=2;
}else{
dp[i][j]=dp[i-1][j];
pre[i][j]=3;
}
}
}
}
if(dp[n][m]!=0) opt(n,m);
return 0;
}
G
DAG 最长路。
设 \(f_i\) 为终点为 \(i\) 时的最长路,则我们可以遍历指向它的所有结点,取 \(\max\) 后 +1。
由于顺序不固定,需要按拓补序遍历。
code
// 版权归 @CuSO4_ 所有
// 未经许可,不许转载
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n,m;
vector<int> g[114514],rg[114514];
int dp[114514],in[114514],in2[114514];
int st[114514],cnt=0;
queue<int> q;
bool vst[114514];
int main(){
ios::sync_with_stdio(false); cin.tie(0);
cin>>n>>m;
for(int i=1;i<=m;i++){
int u,v; cin>>u>>v;
g[u].push_back(v);
rg[v].push_back(u);
in[v]++; in2[v]++;
}
for(int i=1;i<=n;i++){
if(in[i]==0){
q.push(i);
vst[i]=1;
}
}
while(!q.empty()){
int f=q.front(); q.pop();
st[++cnt]=f;
for(int i=0;i<g[f].size();i++){
int v=g[f][i];
in[v]--;
if(in[v]==0&&!vst[v]){
q.push(v);
vst[v]=1;
}
}
}
for(int i=1;i<=n;i++){
int u=st[i];
if(in2[u]==0) continue;
for(int j=0;j<rg[u].size();j++){
dp[u]=max(dp[u],dp[rg[u][j]]);
}
dp[u]++;
}
int ans=0;
for(int i=1;i<=n;i++) ans=max(ans,dp[i]);
cout<<ans<<endl;
return 0;
}
K
设 \(f_{i}\) 为有 \(k\) 个石子时的必胜情况,\(0\) 为先手必败,\(1\) 为先手必胜。
可以发现,如果先手能转移到一个先手必败的局面,则这时的局面就是先手必胜的。
于是可得 \(f_i=f_i\operatorname{or}(\operatorname{not}f_{i-a_j})\;(1\le j\le n)\)。
L
设 \(f_{i,j}\) 为在 \([i,j]\) 区间中的必胜情况,数字越大,对先手越利,反之亦然。
为了方便解答,可将这个值设为题目中的 \(X-Y\)。
此时,先手会尽力让这个值变大,后手会尽力让这个值变小。
此时转移方程就是:
此时用记忆化搜索更简单。
珍爱生命,远离 DP。
code
// 版权归 @CuSO4_ 所有
// 未经许可,不许转载
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int n;
int a[3140];
ll f[3140][3140];
ll dfs(int i,int j,bool isf){
if(i>j) return 0;
if(f[i][j]) return f[i][j];
dfs(i+1,j,!isf);
dfs(i,j-1,!isf);
if(isf){
f[i][j]=max(f[i+1][j]+a[i],f[i][j-1]+a[j]);
}else{
f[i][j]=min(f[i+1][j]-a[i],f[i][j-1]-a[j]);
}
return f[i][j];
}
int main(){
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
cout<<dfs(1,n,1)<<endl;
return 0;
}
N
经典的区间 dp 模板题。
设 \(f_{i,j}\) 为合并 \([i,j]\) 区间内的数的最小代价。
我们可以枚举一个点 \(k\in [i,j)\),表示将 \([i,k]\) 和 \((k,j]\) 的石子合并。
则 \(f_{i,j}=\min\limits_{i\le k<j}\{f_{i,k}+f_{k+1,j}+\sum\limits_{i\le p\le j}\!\!\!a_p\}\)。
注意 \(i\) 要倒序枚举,答案为 \(f_{1,n}\)。
绿 - I,J,M,P,Q,R
I
设 \(f_{i,j}\) 为共有 \(i+j\) 个硬币时投出 \(i\) 个正面和 \(j\) 个反面的概率。
因为每次抛硬币是独立的,所以可以从前面推过来:
其中,蓝色部分表示抛出正面的情况,红色部分表示抛出反面的情况。
因为总共有 \(n\) 枚硬币,所以可以枚举正面的数量 \(i\),并对 \(dp_{i,n-i}\) 求和(其中 \(i>n-i\))。
在递推时注意不要访问到负数下标。
J
与 I 类似,可以设 \(f_{i,j,k}\) 为剩下 \(i\) 个单个寿司,\(j\) 个双拼和 \(k\) 个三拼时的概率。
设 \(s\) 为 \(i+j+k\),则:
-
选到空盘的概率为 \(\dfrac{n}{s}\);
-
选到单盘的概率为 \(f_{i-1,j,k}\cdot\dfrac{i}{s}\);
-
选到双拼的概率为 \(f_{i+1,j-1,k}\cdot\dfrac{j}{s}\);
-
选到三拼的概率为 \(f_{i,j+1,k-1}\cdot\dfrac{k}{s}\)。
以上四个相加即可。结果为 \(f_{\text{cnt}_1,\text{cnt}_2,\text{cnt}_3}\),其中 \(\text{cnt}_i\) 为开始时有 \(i\) 个寿司的盘子的数量。
记得枚举时跳过 \(f_{0,0,0}\)。
M
设 \(f_{i,j}\) 为前 \(i\) 个人分 \(j\) 个糖的方案数。
则 \(f_{i,j}=\sum\limits_{k=j-a_i}^j f_{i-1,k}\)。
但我们显然是不能暴力枚举的。
注意到每次求和都是一个区间,考虑线段树前缀和,时间复杂度降为 \(O(nk)\)。
code
// 版权归 @CuSO4_ 所有
// 未经许可,不许转载
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod=1000000007;
int n,p;
ll a[114];
ll dp[114][114514];
ll pre[114514];
ll query(int l,int r){
return l==0?pre[r]%mod:(pre[r]+mod-pre[l-1])%mod;
}
int main(){
cin>>n>>p;
for(int i=1;i<=n;i++) cin>>a[i];
dp[0][0]=1;
for(int i=1;i<=n;i++){
pre[0]=dp[i-1][0];
for(int j=1;j<=p;j++) pre[j]=(pre[j-1]+dp[i-1][j])%mod;
for(int j=0;j<=p;j++) dp[i][j]=query(max(0ll,j-a[i]),j);
}
cout<<dp[n][p]<<endl;
return 0;
}
P
树型 DP 基本题。
设 \(f_{i,c}\) 为当第 \(i\) 个点的父节点颜色为 \(c\) 时,这个节点的子树的染色方案。(这里可以设 \(1\) 为根,但如果要设为 \(n\) 或 \(\left\lfloor\frac{n}{2}\right\rfloor\) 甚至 rand()%n+1 也不是不行。)
然后就可以得出转移方程(\(F\) 为父节点,\(S\) 为所有子节点):
code
// 版权归 @CuSO4_ 所有
// 未经许可,不许转载
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod=1000000007;
int n;
vector<int> g[114514];
ll mem[114514][2];
ll dfs(int u,int fa,int fc){
if(mem[u][fc]) return mem[u][fc];
if(g[u].size()==1&&fa!=0){
return (fc==1?1:2);
}
ll ans=0,tmp1=1,tmp2=1;
for(int i=0;i<g[u].size();i++){
int v=g[u][i];
if(v==fa) continue;
tmp1=(tmp1%mod)*(dfs(v,u,0)%mod)%mod;
}
ans+=tmp1;
if(fc==0){
for(int i=0;i<g[u].size();i++){
int v=g[u][i];
if(v==fa) continue;
tmp2=(tmp2%mod)*(dfs(v,u,1)%mod)%mod;
}
ans+=tmp2;
}
mem[u][fc]=ans;
return ans;
}
int main(){
cin>>n;
for(int i=1;i<n;i++){
int u,v; cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
}
cout<<dfs(1,0,0)%mod<<endl;
return 0;
}
Q
设 \(f_i\) 为前 \(i\) 个元素的最大和,则转移方程为 \(f_i=\max\limits_{1\le j<i,h_j\le j_i}\{f_j\}+a_i\)。
然而如果直接枚举时间复杂度是 \(O(n^2)\) 的,无法通过。
注意到高度各不相同且 \(\le 2\times 10^5\),我们可以使用一个树状数组来维护这个最大值。
添加时可以将高度作为下标,\(f_i\) 作为权值。
反正只要访问从 1 开始的区间谁管能不能差分呢。
code
// 版权归 @CuSO4_ 所有
// 未经许可,不许转载
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll n;
ll a[202400],h[202400];
ll pmx[202400];
ll dp[202400];
inline ll lb(ll x){return x&(-x);}
void add(ll k,ll p){
while(p<=n){
pmx[p]=max(pmx[p],k);
p+=lb(p);
}
}
ll query(ll p){
ll tmp=0;
while(p>0){
tmp=max(tmp,pmx[p]);
p-=lb(p);
}
return tmp;
}
int main(){
cin>>n;
for(ll i=1;i<=n;i++) cin>>h[i];
for(ll i=1;i<=n;i++) cin>>a[i];
for(ll i=1;i<=n;i++){
dp[i]=query(h[i])+a[i];
add(dp[i],h[i]);
}
ll ans=0;
for(ll i=1;i<=n;i++) ans=max(ans,dp[i]);
cout<<ans<<endl;
return 0;
}
R
对于这道题,我们设 \(f^t_{i,j}\) 表示起点为 \(i\),终点为 \(j\),且长度为 \(t\) 的路径条数。
由乘法原理可得 \(f_{i,j}^t=\sum\limits_{k=1}^{n}f_{i,k}^{t-1}\cdot a_{k,j}\)。
注意到这个式子很像就是矩阵乘法,所以可以把递推过程换成矩阵幂。
也就是说,\(f^t=\bm{A}^t\),其中 \(\bm{A}\) 为初始的邻接矩阵。
答案就是 \(\bm{A}^k\) 的各个元素之和,由于 \(k\le 10^{18}\),要用矩阵快速幂加速,时间复杂度 \(O(n^3\log k)\)。
code
// 版权归 @CuSO4_ 所有
// 未经许可,不许转载
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod=1000000007;
struct mtr{
ll a[114][114];
int n;
mtr(){memset(a,0,sizeof(a));}
mtr operator*(mtr b){
mtr c;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
for(int k=1;k<=n;k++){
c.a[i][j]+=a[i][k]*b.a[k][j];
c.a[i][j]%=mod;
}
}
}
c.n=n;
return c;
}
};
ll n,k;
mtr a;
mtr pow(mtr a,ll k){
mtr res; res.n=n; for(int i=1;i<=n;i++) res.a[i][i]=1;
while(k){
if(k%2==1) res=res*a;
a=a*a;
k/=2;
}
return res;
}
int main(){
cin>>n>>k;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++) cin>>a.a[i][j];
}
a.n=n;
mtr res=pow(a,k);
ll ans=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
ans+=res.a[i][j];
ans%=mod;
}
}
cout<<ans%mod<<endl;
return 0;
}
蓝 - O,S,T,U,W,X,Y
O
注意到 \(n\le 21\),考虑状压。
设 \(f_i\) 表示在 \(i\) 状态下的配对方案数,其中第 \(j\) 个二进制位为 1 表示第 \(j\) 个女孩纸找到了她的 npy,为 0 表示她还没找到。
同时,假设男生是按照 \(1\sim n\) 的顺序依次找 npy 的。
此时,可以让每个单身的女孩纸找一个 npy,设之前的状态为 A,现在的状态为 B,则 \(f_A\) 可以加到 \(f_B\) 上。
但写代码时一般采用倒推。
例如说,\((1111)_2\) 这个状态的方案数是 \((0111)_2\),\((1011)_2\),\((1101)_2\),\((1110)_2\) 其中一些的方案数之和。
但如果这个女孩纸找到的 npy 和她不和,则这个方案数就不能加上去。
最终答案为 \(f_{2^n-1}\)。注意递推时要按 popcount 的的顺序推。
code
// 版权归 @CuSO4_ 所有
// 未经许可,不许转载
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod=1000000007;
int n;
int a[25][25];
int dp[(1<<21)+5];
int popc(int x){
int c=0;
while(x){
if(x&1) c++;
x>>=1;
}
return c;
}
int main(){
cin>>n;
for(int i=1;i<=n;i++) for(int j=0;j<n;j++) cin>>a[i][j];
dp[0]=1;
for(int i=1;i<=n;i++){
for(int j=0;j<(1<<n);j++){
int p=popc(j);
if(p!=i) continue;
for(int k=0;k<n;k++){
if(a[p][k]&&(j&(1<<k))){
dp[j]=(dp[j]+dp[j&(~(1<<k))])%mod;
}
}
}
}
cout<<dp[(1<<n)-1]<<endl;
return 0;
}
注:
1<<n即 \(2^n\);
j&(1<<k)即 \(j\) 的右数第 \(k+1\) 个二进制位 \(\times 2^k\);
j&(~(1<<k))即 \(j\) 去掉右数第 \(k+1\) 个二进制位之后的结果。

浙公网安备 33010602011771号