AtCoder Educational DP Contest A-Z 题解汇总

AtCoder Educational DP Contest A-Z 题解汇总

说明:题目名称、顺序和约束参考 AtCoder Educational DP Contest;洛谷难度评级在网页可抓取文本中不稳定,表中给出的是整理时的参考色级/常见标注,最终以洛谷题面实时显示为准。代码风格尽量保持竞赛写法。

目录与总表

题号 题目 主要算法 洛谷难度评级 跳转
A Frog 1 线性 DP 橙/普及-(参考) 题解
B Frog 2 线性 DP / 枚举转移 橙/普及-(参考) 题解
C Vacation 线性 DP / 状态机 橙/普及-(参考) 题解
D Knapsack 1 01 背包(按重量) 黄/普及+/提高(参考) 题解
E Knapsack 2 01 背包(按价值) 黄/普及+/提高(参考) 题解
F LCS 最长公共子序列 DP + 构造方案 黄/普及+/提高(参考) 题解
G Longest Path DAG DP / 拓扑排序 黄/普及+/提高(参考) 题解
H Grid 1 网格 DP 橙/普及-(参考) 题解
I Coins 概率 DP 黄/普及+/提高(参考) 题解
J Sushi 期望 DP 黄/普及+/提高(参考) 题解
K Stones 博弈 DP 橙/普及-(参考) 题解
L Deque 区间 DP / 博弈分差 黄/普及+/提高(参考) 题解
M Candies 前缀和优化 DP 黄/普及+/提高(参考) 题解
N Slimes 区间 DP 黄/普及+/提高(参考) 题解
O Matching 状压 DP / 完美匹配计数 蓝/提高+/省选-(参考) 题解
P Independent Set 树形 DP 黄/普及+/提高(参考) 题解
Q Flowers 带权 LIS / 树状数组维护最大值 黄/普及+/提高(参考) 题解
R Walk 矩阵快速幂优化 DP 黄/普及+/提高(参考) 题解
S Digit Sum 数位 DP 蓝/提高+/省选-(参考) 题解
T Permutation 排列 DP / 前缀和优化 蓝/提高+/省选-(参考) 题解
U Grouping 状压 DP / 枚举子集划分 蓝/提高+/省选-(参考) 题解
V Subtree 换根 DP / 前后缀积 蓝/提高+/省选-(参考) 题解
W Intervals 线段树优化 DP / 扫描线 蓝/提高+/省选-(参考) 题解
X Tower 排序 + 01 背包 蓝/提高+/省选-(参考) 题解
Y Grid 2 组合计数 DP / 容斥式减障碍 蓝/提高+/省选-(参考) 题解
Z Frog 3 斜率优化 DP / Li Chao 线段树 蓝/提高+/省选-(参考) 题解

A - Frog 1

题目大意

有 \(N\) 块石头,第 \(i\) 块高度为 \(h_i\)。青蛙从 \(1\) 出发,每次可跳到 \(i+1\) 或 \(i+2\),代价为高度差绝对值,求到达 \(N\) 的最小总代价。

数据范围

\(2\le N\le 10^5,\ 1\le h_i\le 10^4\)。

思考方向

到达某块石头的最优代价只和前一块、前两块有关,因此从小到大递推即可。

DP 设计

  • 状态定义:\(dp_i\):到达第 \(i\) 块石头的最小代价。

  • 初始化:\(dp_1=0\),其余初始化为无穷大。

  • 状态转移:\(dp_i=\min(dp_{i-1}+|h_i-h_{i-1}|,dp_{i-2}+|h_i-h_{i-2}|)\),第二项在 \(i>2\) 时存在。

  • 答案:输出 \(dp_N\)。

  • 复杂度:时间 \(O(N)\),空间 \(O(N)\)。

AC 代码

int dp[N],a[N];
void solve(){
	int n;cin>>n;
	memset(dp,0x3f,sizeof(dp));
	for(int i=1;i<=n;i++) cin>>a[i];
	dp[1]=0,dp[2]=abs(a[2]-a[1]);
	for(int i=2;i<=n;i++) dp[i]=min(dp[i-1]+abs(a[i]-a[i-1]),dp[i-2]+abs(a[i]-a[i-2]));
	cout<<dp[n];
}

B - Frog 2

题目大意

Frog 1 的加强版:青蛙在第 \(i\) 块石头时,可以跳到 \(i+1\sim i+K\),代价仍为高度差绝对值。

数据范围

\(2\le N\le 10^5,\ 1\le K\le100,\ 1\le h_i\le10^4\)。

思考方向

因为 \(K\le100\),到达第 \(i\) 块只需要枚举前面最多 \(K\) 个位置。

DP 设计

  • 状态定义:\(dp_i\):到达第 \(i\) 块石头的最小代价。

  • 初始化:\(dp_1=0\),其余为无穷大。

  • 状态转移:\(dp_i=\min_{1\le j\le K,i-j\ge1}{dp_{i-j}+|h_i-h_{i-j}|}\)。

  • 答案:输出 \(dp_N\)。

  • 复杂度:时间 \(O(NK)\),空间 \(O(N)\)。

AC 代码

int a[N],dp[N];
int n,k;
void solve(){
	cin>>n>>k;
	for(int i=1;i<=n;i++) cin>>a[i];
	memset(dp,0x3f,sizeof(dp));
	dp[1]=0;
	for(int i=2;i<=n;i++){
		for(int j=1;j<=k&&j<i;j++){
			dp[i]=min(dp[i],dp[i-j]+abs(a[i]-a[i-j]));
		}
	}
	cout<<dp[n];
}

C - Vacation

题目大意

有 \(N\) 天,每天从三种活动中选一种,获得对应幸福值,不能连续两天选择同一种活动,求最大总幸福值。

数据范围

\(1\le N\le10^5,\ 1\le a_i,b_i,c_i\le10^4\)。

思考方向

每天的限制只和昨天选了什么有关,所以记录“第 \(i\) 天选哪一种活动”即可。

DP 设计

  • 状态定义:\(dp_{i,j}\):前 \(i\) 天,第 \(i\) 天选择活动 \(j\) 时的最大幸福值,\(j=0,1,2\)。

  • 初始化:第 \(1\) 天:\(dp_{1,0}=a_1,dp_{1,1}=b_1,dp_{1,2}=c_1\)。

  • 状态转移:\(dp_{i,j}=val_{i,j}+\max_{k\ne j}dp_{i-1,k}\)。

  • 答案:输出 \(\max(dp_{N,0},dp_{N,1},dp_{N,2})\)。

  • 复杂度:时间 \(O(N)\),空间 \(O(N)\),也可滚动。

AC 代码

int dp[N][3],a[N][3];
void solve(){
	int n;cin>>n;
	for(int i=1;i<=n;i++){
		for(int j=0;j<3;j++) cin>>a[i][j];
	}
	for(int i=1;i<=n;i++){
		for(int j=0;j<3;j++){
			for(int k=0;k<3;k++){
				if(j!=k) dp[i][j]=max(dp[i][j],dp[i-1][k]+a[i][k]);
			}
		}
	}
	cout<<max({dp[n][0],dp[n][1],dp[n][2]});
}

D - Knapsack 1

题目大意

有 \(N\) 个物品,每个物品有重量 \(w_i\) 和价值 \(v_i\),背包容量为 \(W\),每个物品最多选一次,求最大价值。

数据范围

\(1\le N\le100,\ 1\le W\le10^5,\ 1\le w_i\le W,\ 1\le v_i\le10^9\)。

思考方向

容量 \(W\) 可承受,直接做按重量的一维 01 背包。价值可能很大,所以答案用 long long

DP 设计

  • 状态定义:\(dp_j\):当前处理过的物品中,总重量不超过 \(j\) 时的最大价值。

  • 初始化:全部为 \(0\)。

  • 状态转移:枚举物品 \(i\),倒序枚举容量:\(dp_j=\max(dp_j,dp_{j-w_i}+v_i)\)。倒序是为了保证每个物品只用一次。

  • 答案:输出 \(dp_W\)。

  • 复杂度:时间 \(O(NW)\),空间 \(O(W)\)。

AC 代码

int n,W;
int dp[N];
int w[N],v[N];
void solve(){
	cin>>n>>W;
	for(int i=1;i<=n;i++) cin>>w[i]>>v[i];
	for(int i=1;i<=n;i++){
		for(int j=W;j>=w[i];j--){
			dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
		}
	}
	cout<<dp[W];
}

E - Knapsack 2

题目大意

仍是 01 背包,但 \(W\) 很大,直接按重量做不可行;每个物品价值较小。

数据范围

\(1\le N\le100,\ 1\le W\le10^9,\ 1\le w_i\le W,\ 1\le v_i\le10^3\),总价值上界约 \(10^5\)。

思考方向

重量范围大,价值范围小,于是把状态改成“达到某个价值的最小重量”。

DP 设计

  • 状态定义:\(dp_j\):总价值恰好为 \(j\) 时的最小重量。

  • 初始化:\(dp_0=0\),其余为无穷大。

  • 状态转移:枚举物品,倒序枚举价值:\(dp_j=\min(dp_j,dp_{j-v_i}+w_i)\)。

  • 答案:找最大的 \(j\),满足 \(dp_j\le W\)。

  • 复杂度:时间 \(O(N\sum v_i)\),空间 \(O(\sum v_i)\)。

AC 代码

int n,W,w[105],v[105],dp[N];
void solve(){
	cin>>n>>W;
	int sum=0;
	for(int i=1;i<=n;i++){
		cin>>w[i]>>v[i];
		sum+=v[i];
	}
	memset(dp,0x3f,sizeof(dp));
	dp[0]=0;
	for(int i=1;i<=n;i++)
		for(int j=sum;j>=v[i];j--)	
			dp[j]=min(dp[j],dp[j-v[i]]+w[i]);
	int ans=0;
	for(int i=0;i<=sum;i++)
		if(dp[i]<=W) ans=max(ans,i);
	cout<<ans;
}

F - LCS

题目大意

给定两个字符串 \(s,t\),求一个最长公共子序列。

数据范围

\(1\le |s|,|t|\le3000\)。

思考方向

先用经典 LCS 求长度,再从右下角反向走,还原一个方案。

DP 设计

  • 状态定义:\(dp_{i,j}\):\(s\) 的前 \(i\) 个字符和 \(t\) 的前 \(j\) 个字符的 LCS 长度。

  • 初始化:\(dp_{0,j}=dp_{i,0}=0\)。

  • 状态转移:若 \(s_i=t_j\),\(dp_{i,j}=dp_{i-1,j-1}+1\);否则 \(dp_{i,j}=\max(dp_{i-1,j},dp_{i,j-1})\)。

  • 答案:从 \((n,m)\) 逆推:相等就加入答案并斜着走,否则向较大的来源走。

  • 复杂度:时间 \(O(|s||t|)\),空间 \(O(|s||t|)\)。

AC 代码

//dp[i][j]:s前i和t前j个字符的LCS长度
//dp[0][j]=dp[i][0]=0
//s[i]==t[j]:dp[i][j]=dp[i-1][j-1]+1,else:dp[i][j]=max(dp[i][j-1],dp[i-1][j])
int dp[3005][3005];
void solve(){
	string s,t;cin>>s>>t;
	int n=s.size(),m=t.size();
	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;
			else dp[i][j]=max(dp[i][j-1],dp[i-1][j]);
		}
	}
	string ans;
	int i=n,j=m;
	while(i>=1&&j>=1){
		if(s[i]==t[j]) ans+=s[i],i--,j--;
		else if(dp[i-1][j]>=dp[i][j-1]) i--;
		else j--;
	}
	reverse(ans.begin(),ans.end());
	cout<<ans<<endl;
}

G - Longest Path

题目大意

给定一个有向无环图,求最长有向路径的边数。

数据范围

\(1\le N,M\le10^5\),图保证为 DAG。

思考方向

DAG 上最长路可以按拓扑序转移:一条边 \(u\to v\) 表示 \(v\) 可以由 \(u\) 延长一条边。

DP 设计

  • 状态定义:\(dp_v\):以 \(v\) 为终点的最长路径长度。

  • 初始化:入度为 \(0\) 的点初始为 \(0\),其余也可设 \(0\)。

  • 状态转移:按拓扑序遍历边 \(u\to v\),\(dp_v=\max(dp_v,dp_u+1)\)。

  • 答案:输出所有 \(dp_v\) 的最大值。

  • 复杂度:时间 \(O(N+M)\),空间 \(O(N+M)\)。

AC 代码

int n,m,in[N],dp[N];
vector<int> g[N];
#define pb push_back
void solve(){
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		int u,v;cin>>u>>v;
		g[u].pb(v);
		in[v]++;
	}
	queue<int> q;
	for(int i=1;i<=n;i++) if(!in[i]) q.push(i);
	while(q.size()){
		int u=q.front();q.pop();
		for(int v:g[u]){
			dp[v]=max(dp[v],dp[u]+1);
			if((--in[v])==0) q.push(v);
		}
	}
	int ans=0;
	for(int i=1;i<=n;i++) ans=max(ans,dp[i]);
	cout<<ans<<endl;
}

H - Grid 1

题目大意

给定 \(H\times W\) 网格,. 可走,# 不可走。从左上角走到右下角,每次只能向右或向下,求方案数。

数据范围

\(1\le H,W\le1000\)。

思考方向

到达一个格子的路径只能来自上方或左方,遇到障碍则方案数为 \(0\)。

DP 设计

  • 状态定义:\(dp_{i,j}\):到达 \((i,j)\) 的方案数。

  • 初始化:若起点可走,\(dp_{1,1}=1\)。

  • 状态转移:若 \((i,j)\) 可走,\(dp_{i,j}=dp_{i-1,j}+dp_{i,j-1}\)。

  • 答案:输出 \(dp_{H,W}\),取模 \(10^9+7\)。

  • 复杂度:时间 \(O(HW)\),空间 \(O(HW)\)。

AC 代码

int h,w,dp[N][N];
char a[N][N];
void solve(){
	cin>>h>>w;
	for(int i=1;i<=h;i++) for(int j=1;j<=w;j++) cin>>a[i][j];
	dp[1][1]=(a[1][1]=='.');
	for(int i=1;i<=h;i++){
		for(int j=1;j<=w;j++){
			if(a[i][j]=='#') dp[i][j]=0;
			else{
				if(i>1) dp[i][j]=(dp[i][j]+dp[i-1][j])%mod;
				if(j>1) dp[i][j]=(dp[i][j]+dp[i][j-1])%mod;
			}
		}
	}
	cout<<dp[h][w];
}

I - Coins

题目大意

有 \(N\) 枚硬币,第 \(i\) 枚正面概率为 \(p_i\)。求正面数量严格大于反面数量的概率。

数据范围

\(1\le N\le2999\),\(0<p_i<1\)。

思考方向

投完前 \(i\) 枚后,正面数为 \(j\) 的概率可以由上一枚正面/反面两种情况转移。

DP 设计

  • 状态定义:\(dp_j\):当前已经处理的硬币中,正面数为 \(j\) 的概率。

  • 初始化:\(dp_0=1\)。

  • 状态转移:处理概率 \(p\) 时:\(ndp_j=dp_{j-1}p+dp_j(1-p)\)。

  • 答案:累加 \(j>N/2\) 的 \(dp_j\)。

  • 复杂度:时间 \(O(N^2)\),空间 \(O(N)\)。

AC 代码

int n;
double dp[2][N];//dp[i&1][j]:投完第i枚后,正面数为j的概率
void solve(){
	cin>>n;
	dp[0][0]=1.0;
	for(int i=1;i<=n;i++){
		double p;cin>>p;
		memset(dp[i&1],0,sizeof(dp[i&1]));
		dp[i&1][0]=dp[(i-1)&1][0]*(1-p);
		for(int j=1;j<=i;j++){
			dp[i&1][j]=dp[(i-1)&1][j-1]*p+dp[(i-1)&1][j]*(1-p);
		}
	}
	double ans=0;
	for(int j=n/2+1;j<=n;j++) ans+=dp[n&1][j];
	cout<<fixed<<setprecision(10)<<ans<<endl;
}

J - Sushi

题目大意

有 \(N\) 个盘子,每个盘子初始有 \(1,2,3\) 个寿司。每次等概率选一个盘子,若有寿司就吃一个,求吃完所有寿司的期望操作次数。

数据范围

\(1\le N\le300\),每盘寿司数为 \(1,2,3\)。

思考方向

直接按具体盘子状态不可行,但每次只关心有 \(1,2,3\) 个寿司的盘子数量。期望式中会出现“选到空盘子仍停留在原状态”,移项即可。

DP 设计

  • 状态定义:\(dp_{i,j,k}\):当前有 \(i\) 个一寿司盘、\(j\) 个二寿司盘、\(k\) 个三寿司盘时,吃完的期望次数。

  • 初始化:\(dp_{0,0,0}=0\)。

  • 状态转移:设 \(s=i+j+k\),则
    \(dp_{i,j,k}=\dfrac{N+i,dp_{i-1,j,k}+j,dp_{i+1,j-1,k}+k,dp_{i,j+1,k-1}}{s}\)。

  • 答案:统计初始 \(c_1,c_2,c_3\),输出 \(dp_{c_1,c_2,c_3}\)。

  • 复杂度:状态数 \(O(N^3)\),转移 \(O(1)\)。

AC 代码

int n,cnt[4];
double dp[N][N][N];
//dp[i][j][k]:1,2,3盘的数量分别为i,j,k,从这个状态开始,吃掉所有寿司的期望操作次数
//dp[i][j][k]=1
// +(N-s)/N*dp[i][j][k]
// +i/N*dp[i-1][j][k]
// +j/N*dp[i+1][j-1][k]
// +k/N*dp[i][j+1][k-1]
void solve(){
	cin>>n;
	for(int i=1;i<=n;i++){
		int x;cin>>x;cnt[x]++;
	}
	for(int k=0;k<=n;k++){
		for(int j=0;j<=n;j++){
			for(int i=0;i<=n;i++){
				int s=i+j+k;
				if(!s||s>n) continue;
				double res=n;
				if(i) res+=i*dp[i-1][j][k];
				if(j) res+=j*dp[i+1][j-1][k];
				if(k) res+=k*dp[i][j+1][k-1];
				dp[i][j][k]=res/s;
			}
		}
	}
	cout<<fixed<<setprecision(10)<<dp[cnt[1]][cnt[2]][cnt[3]];
}

K - Stones

题目大意

有 \(K\) 个石子,允许每次取走集合 \(A\) 中的一种数量。不能操作者输,判断先手胜负。

数据范围

\(1\le N\le100,\ 1\le K\le10^5\),\(1\le a_i\le K\)。

思考方向

这是普通必胜/必败状态 DP:能转移到任意必败状态,则当前为必胜。

DP 设计

  • 状态定义:\(dp_i\):剩下 \(i\) 个石子,当前操作方是否必胜。

  • 初始化:\(dp_0=0\),无石子可取,当前方必败。

  • 状态转移:若存在 \(a_j\le i\) 且 \(dp_{i-a_j}=0\),则 \(dp_i=1\)。

  • 答案:\(dp_K=1\) 输出 First,否则输出 Second

  • 复杂度:时间 \(O(NK)\),空间 \(O(K)\)。

AC 代码

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
const int N=1e5+5;
int n,k,a[105],dp[N];
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cin>>n>>k;
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int i=1;i<=k;i++){
		for(int j=1;j<=n;j++){
			if(i>=a[j]&&!dp[i-a[j]]) dp[i]=1;
		}
	}
	cout<<(dp[k]?"First":"Second")<<endl;
	return 0;
}

L - Deque

题目大意

两人轮流从序列两端取数,先手最大化自己的总和减后手总和,后手也最优,求最终分差。

数据范围

\(1\le N\le3000,\ 1\le a_i\le10^9\)。

思考方向

用“当前玩家相对另一个玩家的最大分差”做状态,就不需要额外记录轮次。

DP 设计

  • 状态定义:\(dp_{l,r}\):面对区间 \([l,r]\),当前玩家最终能取得的最大分差。

  • 初始化:\(dp_{i,i}=a_i\)。

  • 状态转移:取左端后,对手面对 \([l+1,r]\),所以分差为 \(a_l-dp_{l+1,r}\);取右端为 \(a_r-dp_{l,r-1}\)。取最大。

  • 答案:输出 \(dp_{1,N}\)。

  • 复杂度:时间 \(O(N^2)\),空间 \(O(N^2)\)。

AC 代码

//dp[i][j]:在剩下的序列为a[i...j]时,当前操作的玩家,能获得的(自己的分数-对手的分数)的最大值。
//dp[i][j]=max(a[i]-dp[i+1][j],a[j]-dp[i][j-1])
int a[N],dp[N][N];
void solve(){
	int n;cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int i=1;i<=n;i++) dp[i][i]=a[i];
	//区间 DP 常规套路:先枚举区间长度,再枚举左端点
	for(int len=2;len<=n;len++){
		for(int i=1;i+len-1<=n;i++){
			int j=i+len-1;
			dp[i][j]=max(a[i]-dp[i+1][j],a[j]-dp[i][j-1]);
		}
	}
	cout<<dp[1][n]<<endl;
}

M - Candies

题目大意

有 \(N\) 个孩子,第 \(i\) 个孩子最多分 \(a_i\) 颗糖,总共分 \(K\) 颗,求方案数。

数据范围

\(1\le N\le100,\ 0\le K\le10^5,\ 0\le a_i\le K\)。

思考方向

朴素转移要枚举每个孩子分多少,复杂度过高。注意转移是连续区间求和,用前缀和优化。

DP 设计

  • 状态定义:\(dp_j\):处理到当前孩子前,已经分出 \(j\) 颗糖的方案数。

  • 初始化:\(dp_0=1\)。

  • 状态转移:处理 \(a_i\) 时,\(ndp_j=\sum_{x=0}^{a_i}dp_{j-x}\),用前缀和变为区间和。

  • 答案:输出 \(dp_K\)。

  • 复杂度:时间 \(O(NK)\),空间 \(O(K)\)。

AC 代码

//dp[i&1][j]:分到第i个孩子已经给出j个的方案数
//dp[i&1][j]=sigema(x=0->a[i])dp[i-1][j-x]
int n,k,dp[2][N],pre[N];
void solve(){
	cin>>n>>k;
	dp[0][0]=1;
	for(int i=1;i<=n;i++){
		int a;cin>>a;
		pre[0]=dp[(i-1)&1][0];
		for(int j=1;j<=k;j++) pre[j]=(pre[j-1]+dp[(i-1)&1][j])%mod;
		for(int j=0;j<=k;j++){
			int l=max(0ll,j-a),r=j;
			if(l) dp[i&1][j]=(pre[r]-pre[l-1]+mod)%mod;
			else dp[i&1][j]=pre[r]%mod;
		} 
	}
	cout<<dp[n&1][k];
}

N - Slimes

题目大意

有 \(N\) 堆史莱姆,每次可以合并相邻两堆,代价为两堆大小之和,求合并成一堆的最小总代价。

数据范围

\(2\le N\le400,\ 1\le a_i\le10^9\)。

思考方向

最后一次合并一定把某个区间 \([l,r]\) 分成 \([l,k]\) 与 \([k+1,r]\) 两部分,枚举断点即可。

DP 设计

  • 状态定义:\(dp_{l,r}\):把 \([l,r]\) 合并成一堆的最小代价。

  • 初始化:\(dp_{i,i}=0\)。

  • 状态转移:\(dp_{l,r}=\min_k(dp_{l,k}+dp_{k+1,r}+sum(l,r))\)。

  • 答案:输出 \(dp_{1,N}\)。

  • 复杂度:时间 \(O(N^3)\),空间 \(O(N^2)\)。

AC 代码

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
const int N=405;
const int INF=0x3f3f3f3f3f3f3f3f;
int n,a[N],s[N],dp[N][N];
int sum(int l,int r){return s[r]-s[l-1];}
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i],s[i]=s[i-1]+a[i];
	for(int len=2;len<=n;len++){
		for(int l=1;l+len-1<=n;l++){
			int r=l+len-1;
			dp[l][r]=INF;
			for(int k=l;k<r;k++) dp[l][r]=min(dp[l][r],dp[l][k]+dp[k+1][r]+sum(l,r));
		}
	}
	cout<<dp[1][n]<<endl;
	return 0;
}

O - Matching

题目大意

有 \(N\) 个男生和 \(N\) 个女生,\(a_{i,j}=1\) 表示男生 \(i\) 可以与女生 \(j\) 配对,求完美匹配方案数。

数据范围

\(1\le N\le21\)。

思考方向

\(N\) 小,女生集合可以状压。若当前已经匹配了若干女生,那么下一个男生编号由集合大小决定。

DP 设计

  • 状态定义:\(dp_S\):已经给前 \(|S|\) 个男生匹配了女生集合 \(S\) 的方案数。

  • 初始化:\(dp_0=1\)。

  • 状态转移:设 \(i=|S|+1\),枚举未选女生 \(j\),若 \(a_{i,j}=1\),则 \(dp_{S\cup{j}}+=dp_S\)。

  • 答案:输出 \(dp_{2^N-1}\)。

  • 复杂度:时间 \(O(N2^N)\),空间 \(O(2^N)\)。

AC 代码

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
const int mod=1e9+7;
int n,a[25][25];
vector<int> dp;
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cin>>n;
	for(int i=1;i<=n;i++) for(int j=0;j<n;j++) cin>>a[i][j];
	int lim=1LL<<n;
	dp.assign(lim,0);
	dp[0]=1;
	for(int s=0;s<lim;s++){
		int i=__builtin_popcountll(s)+1;
		if(i>n) continue;
		for(int j=0;j<n;j++){
			if(!(s>>j&1)&&a[i][j]){
				dp[s|(1LL<<j)]=(dp[s|(1LL<<j)]+dp[s])%mod;
			}
		}
	}
	cout<<dp[lim-1]<<endl;
	return 0;
}

P - Independent Set

题目大意

给定一棵树,每个点染黑或白,要求没有相邻黑点,求合法染色方案数。

数据范围

\(1\le N\le10^5\)。

思考方向

树上相邻限制只在父子之间发生。若当前点黑,则所有儿子必须白;若当前点白,儿子可黑可白。

DP 设计

  • 状态定义:\(dp_{u,0}\):\(u\) 染白的子树方案数;\(dp_{u,1}\):\(u\) 染黑的子树方案数。

  • 初始化:叶子:\(dp_{u,0}=dp_{u,1}=1\)。

  • 状态转移:\(dp_{u,0}=\prod_v(dp_{v,0}+dp_{v,1})\),\(dp_{u,1}=\prod_v dp_{v,0}\)。

  • 答案:输出 \(dp_{1,0}+dp_{1,1}\)。

  • 复杂度:时间 \(O(N)\),空间 \(O(N)\)。

AC 代码

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
const int mod=1e9+7;
const int N=1e5+5;
int n,fa[N],dp[N][2];
vector<int> g[N],ord;
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	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);
	}
	ord.push_back(1);
	for(int i=0;i<(int)ord.size();i++){
		int u=ord[i];
		for(int v:g[u]) if(v!=fa[u]) fa[v]=u,ord.push_back(v);
	}
	for(int i=n-1;i>=0;i--){
		int u=ord[i];
		dp[u][0]=dp[u][1]=1;
		for(int v:g[u]) if(v!=fa[u]){
			dp[u][0]=dp[u][0]*(dp[v][0]+dp[v][1])%mod;
			dp[u][1]=dp[u][1]*dp[v][0]%mod;
		}
	}
	cout<<(dp[1][0]+dp[1][1])%mod<<endl;
	return 0;
}

Q - Flowers

题目大意

有 \(N\) 朵花,第 \(i\) 朵高度 \(h_i\)、美丽值 \(a_i\)。选择一个高度严格递增的子序列,最大化美丽值总和。

数据范围

\(1\le N\le2\times10^5\),\(h_i\) 为 \(1\sim N\) 的排列,\(1\le a_i\le10^9\)。

思考方向

这是带权 LIS。以当前花结尾时,需要找之前所有高度小于它的最大 dp,用树状数组维护前缀最大值。

DP 设计

  • 状态定义:\(dp_i\):选择第 \(i\) 朵花作为结尾的最大美丽值。树状数组 \(tr_x\) 维护高度前缀最大值。

  • 初始化:树状数组初始全 \(0\)。

  • 状态转移:\(dp_i=query(h_i-1)+a_i\),然后在高度 \(h_i\) 处更新最大值。

  • 答案:所有 \(dp_i\) 的最大值。

  • 复杂度:时间 \(O(N\log N)\),空间 \(O(N)\)。

AC 代码

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
const int N=2e5+5;
int n,h[N],a[N],tr[N];
int lowbit(int x){return x&-x;}
void add(int x,int v){for(;x<=n;x+=lowbit(x)) tr[x]=max(tr[x],v);}
int query(int x){int res=0;for(;x;x-=lowbit(x)) res=max(res,tr[x]);return res;}
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cin>>n;
	for(int i=1;i<=n;i++) cin>>h[i];
	for(int i=1;i<=n;i++) cin>>a[i];
	int ans=0;
	for(int i=1;i<=n;i++){
		int dp=query(h[i]-1)+a[i];
		ans=max(ans,dp);
		add(h[i],dp);
	}
	cout<<ans<<endl;
	return 0;
}

R - Walk

题目大意

给定 \(N\) 个点的有向图邻接矩阵,求长度恰好为 \(K\) 的有向走法总数。

数据范围

\(1\le N\le50,\ 1\le K\le10^{18}\)。

思考方向

邻接矩阵 \(A\) 的 \(K\) 次幂中,\((i,j)\) 表示从 \(i\) 到 \(j\) 长度为 \(K\) 的走法数。对矩阵快速幂即可。

DP 设计

  • 状态定义:矩阵乘法本质是 DP 合并:\(C_{i,j}=\sum_k A_{i,k}B_{k,j}\)。

  • 初始化:快速幂单位矩阵为初始答案矩阵。

  • 状态转移:二进制拆分 \(K\),不断平方转移矩阵。

  • 答案:求 \(A^K\) 所有元素之和。

  • 复杂度:时间 \(O(N^3\log K)\),空间 \(O(N^2)\)。

AC 代码

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
const int mod=1e9+7;
const int N=55;
int n,k;
struct mat{int a[N][N];};
mat mul(mat x,mat y){
	mat z;
	memset(z.a,0,sizeof z.a);
	for(int i=1;i<=n;i++){
		for(int k=1;k<=n;k++) if(x.a[i][k]){
			for(int j=1;j<=n;j++) z.a[i][j]=(z.a[i][j]+x.a[i][k]*y.a[k][j])%mod;
		}
	}
	return z;
}
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cin>>n>>k;
	mat a,res;
	memset(a.a,0,sizeof a.a);
	memset(res.a,0,sizeof res.a);
	for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) cin>>a.a[i][j];
	for(int i=1;i<=n;i++) res.a[i][i]=1;
	while(k){
		if(k&1) res=mul(res,a);
		a=mul(a,a);
		k>>=1;
	}
	int ans=0;
	for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) ans=(ans+res.a[i][j])%mod;
	cout<<ans<<endl;
	return 0;
}

S - Digit Sum

题目大意

给定正整数上界 \(K\) 和整数 \(D\),求 \(1\sim K\) 中,数位和能被 \(D\) 整除的数的个数。

数据范围

\(K\) 的位数 \(\le10^4\),\(1\le D\le100\)。

思考方向

典型数位 DP。从左到右处理,记录当前数位和模 \(D\),以及是否已经小于上界。

DP 设计

  • 状态定义:\(dp_{r,0/1}\):已处理前缀的数位和模 \(D\) 为 \(r\),第二维表示是否已经小于 \(K\)。

  • 初始化:\(dp_{0,0}=1\)。

  • 状态转移:枚举当前位填 \(0\sim limit\),更新余数和 tight/less 标记。

  • 答案:答案为 \(dp_{0,0}+dp_{0,1}-1\),减去数字 \(0\)。

  • 复杂度:时间 \(O(|K|D10)\),空间 \(O(D)\)。

AC 代码

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
const int mod=1e9+7;
int d,dp[105][2],ndp[105][2];
string k;
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cin>>k>>d;
	dp[0][0]=1;
	for(char c:k){
		memset(ndp,0,sizeof ndp);
		int up=c-'0';
		for(int r=0;r<d;r++){
			for(int less=0;less<=1;less++){
				int lim=less?9:up;
				for(int x=0;x<=lim;x++){
					int nr=(r+x)%d;
					int nl=less||(x<up);
					ndp[nr][nl]=(ndp[nr][nl]+dp[r][less])%mod;
				}
			}
		}
		memcpy(dp,ndp,sizeof dp);
	}
	cout<<(dp[0][0]+dp[0][1]-1+mod)%mod<<endl;
	return 0;
}

T - Permutation

题目大意

给定长度 \(N-1\) 的 <> 字符串,求 \(1\sim N\) 的排列中满足所有相邻大小关系的数量。

数据范围

\(2\le N\le3000\)。

思考方向

逐步插入第 \(i\) 个数,不关心具体值,只关心最后一个数在当前前缀中的排名。转移是排名区间求和,用前缀和优化。

DP 设计

  • 状态定义:\(dp_j\):当前长度为 \(i\) 的前缀中,最后一个数排名为 \(j\) 的方案数。

  • 初始化:长度为 \(1\) 时,\(dp_1=1\)。

  • 状态转移:加入新数后排名为 \(j\)。若符号为 <,上一位排名必须 \(<j\);若为 >,上一位排名必须 \(\ge j\)。用前缀和快速求和。

  • 答案:长度为 \(N\) 后,所有排名方案数求和。

  • 复杂度:时间 \(O(N^2)\),空间 \(O(N)\)。

AC 代码

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
const int mod=1e9+7;
const int N=3005;
int n,dp[N],ndp[N],pre[N];
string s;
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cin>>n>>s;
	dp[1]=1;
	for(int len=1;len<n;len++){
		for(int i=1;i<=len;i++) pre[i]=(pre[i-1]+dp[i])%mod;
		for(int j=1;j<=len+1;j++){
			if(s[len-1]=='<') ndp[j]=pre[j-1];
			else ndp[j]=(pre[len]-pre[j-1]+mod)%mod;
		}
		for(int j=1;j<=len+1;j++) dp[j]=ndp[j],ndp[j]=0;
		for(int j=0;j<=len;j++) pre[j]=0;
	}
	int ans=0;
	for(int j=1;j<=n;j++) ans=(ans+dp[j])%mod;
	cout<<ans<<endl;
	return 0;
}

U - Grouping

题目大意

有 \(N\) 个人,两两在同一组会产生贡献 \(a_{i,j}\)。把所有人划分成若干组,最大化组内贡献总和。

数据范围

\(1\le N\le16\),\(a_{i,j}\) 可为负。

思考方向

先预处理每个集合内部所有二元贡献之和,再做集合划分 DP。枚举当前集合的一个子集作为最后一组。

DP 设计

  • 状态定义:\(val_S\):集合 \(S\) 内所有点放在同一组的贡献。\(dp_S\):划分集合 \(S\) 的最大贡献。

  • 初始化:\(dp_0=0\)。

  • 状态转移:\(dp_S=\max_{T\subseteq S}{dp_{S\setminus T}+val_T}\)。为了减少重复,枚举 \(T\) 时强制包含 \(S\) 的最低位。

  • 答案:输出 \(dp_{2^N-1}\)。

  • 复杂度:预处理 \(O(N^2 2^N)\) 或直接枚举对;DP 约 \(O(3^N)\)。

AC 代码

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
const int INF=0x3f3f3f3f3f3f3f3f;
int n,a[20][20],val[1<<16],dp[1<<16];
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cin>>n;
	for(int i=0;i<n;i++) for(int j=0;j<n;j++) cin>>a[i][j];
	int lim=1LL<<n;
	for(int s=0;s<lim;s++){
		for(int i=0;i<n;i++) if(s>>i&1){
			for(int j=i+1;j<n;j++) if(s>>j&1) val[s]+=a[i][j];
		}
	}
	for(int s=1;s<lim;s++) dp[s]=-INF;
	for(int s=1;s<lim;s++){
		int b=s&-s;
		for(int t=s;t;t=(t-1)&s){
			if(t&b) dp[s]=max(dp[s],dp[s^t]+val[t]);
		}
	}
	cout<<dp[lim-1]<<endl;
	return 0;
}

V - Subtree

题目大意

给定一棵树。对每个顶点 \(v\),统计所有黑点连通且包含 \(v\) 的染色方案数,答案对给定 \(M\) 取模。

数据范围

\(1\le N\le10^5,\ 2\le M\le10^9\)。

思考方向

固定根时,子树内包含当前点的连通黑块可以从每个儿子方向选择“不要”或“选择一个包含儿子的连通块”。对每个点都要求答案,需要换根。因为 \(M\) 不一定是质数,不能用除法,改用前后缀积。

DP 设计

  • 状态定义:\(down_u\):以 \(1\) 为根时,\(u\) 子树内包含 \(u\) 的连通黑块方案数。\(up_u\):从 \(u\) 的父亲方向那一侧,包含父亲并与 \(u\) 相邻可连接的方案数。

  • 初始化:叶子 \(down=1\);根的 \(up=0\),表示没有父亲方向。

  • 状态转移:\(down_u=\prod_{child}(down_v+1)\)。对邻居贡献统一看成 \(contrib+1\),答案是所有方向的乘积;给儿子传 \(up\) 时排除这个儿子方向,其余方向乘起来即可。

  • 答案:每个点输出 \(\prod_{neighbor}(contrib+1)\)。

  • 复杂度:时间 \(O(N)\),空间 \(O(N)\)。

AC 代码

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
const int N=1e5+5;
int n,mo,fa[N],down[N],up[N],ans[N];
vector<int> g[N],ord;
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cin>>n>>mo;
	for(int i=1;i<n;i++){
		int u,v;
		cin>>u>>v;
		g[u].push_back(v);
		g[v].push_back(u);
	}
	ord.push_back(1);
	for(int i=0;i<(int)ord.size();i++){
		int u=ord[i];
		for(int v:g[u]) if(v!=fa[u]) fa[v]=u,ord.push_back(v);
	}
	for(int i=n-1;i>=0;i--){
		int u=ord[i];
		down[u]=1%mo;
		for(int v:g[u]) if(v!=fa[u]) down[u]=down[u]*(down[v]+1)%mo;
	}
	up[1]=0;
	for(int u:ord){
		int d=g[u].size();
		vector<int> val(d),pre(d+1,1%mo),suf(d+1,1%mo);
		for(int i=0;i<d;i++){
			int v=g[u][i];
			val[i]=(v==fa[u]?up[u]:down[v]);
			pre[i+1]=pre[i]*(val[i]+1)%mo;
		}
		for(int i=d-1;i>=0;i--) suf[i]=suf[i+1]*(val[i]+1)%mo;
		ans[u]=pre[d];
		for(int i=0;i<d;i++){
			int v=g[u][i];
			if(v!=fa[u]) up[v]=pre[i]*suf[i+1]%mo;
		}
	}
	for(int i=1;i<=n;i++) cout<<ans[i]%mo<<endl;
	return 0;
}

W - Intervals

题目大意

给定 \(N\) 个位置和 \(M\) 个带权区间。选择一个 01 串,若区间内至少有一个 1,就得到该区间权值,求最大得分。

数据范围

\(1\le N,M\le2\times10^5\),权值可正可负。

思考方向

设当前扫到右端点 \(i\),用“最后一个 1 的位置”作为状态。区间 \([l,i]\) 在右端点被处理时,只有最后一个 1 在 \([l,i]\) 的状态能获得这个区间贡献,因此可以对一段状态做区间加。每次新开一个最后位置 \(i\),它可以从之前所有状态最大值转移。

DP 设计

  • 状态定义:线段树位置 \(j\) 表示最后一个 1 在 \(j\) 时的当前最大分数,\(j=0\) 表示还没选任何位置。

  • 初始化:初始 \(pos=0\) 为 \(0\),其余位置为 \(-\infty\)。

  • 状态转移:扫 \(i=1\sim N\):先令 \(dp_i=\max_{0\le j<i}dp_j\);然后处理所有右端点为 \(i\) 的区间 \([l,i]\),对线段树区间 \([l,i]\) 加上权值。

  • 答案:输出所有状态最大值,允许一个位置都不选,所以答案至少为 \(0\)。

  • 复杂度:时间 \(O((N+M)\log N)\),空间 \(O(N+M)\)。

AC 代码

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
const long long INF=4000000000000000000LL;
const int N=2e5+5;
struct seg{int mx,lazy;}tr[N*4];
struct node{int l,a;};
int n,m;
vector<node> e[N];
void pushup(int x){tr[x].mx=max(tr[x<<1].mx,tr[x<<1|1].mx);}
void addtag(int x,int v){tr[x].mx+=v;tr[x].lazy+=v;}
void pushdown(int x){
	if(!tr[x].lazy) return;
	addtag(x<<1,tr[x].lazy);
	addtag(x<<1|1,tr[x].lazy);
	tr[x].lazy=0;
}
void build(int x,int l,int r){
	tr[x].lazy=0;
	if(l==r){tr[x].mx=(l==0?0:-INF);return;}
	int mid=l+r>>1;
	build(x<<1,l,mid);build(x<<1|1,mid+1,r);
	pushup(x);
}
void change(int x,int l,int r,int ql,int qr,int v){
	if(ql<=l&&r<=qr){addtag(x,v);return;}
	pushdown(x);
	int mid=l+r>>1;
	if(ql<=mid) change(x<<1,l,mid,ql,qr,v);
	if(qr>mid) change(x<<1|1,mid+1,r,ql,qr,v);
	pushup(x);
}
int query(int x,int l,int r,int ql,int qr){
	if(ql<=l&&r<=qr) return tr[x].mx;
	pushdown(x);
	int mid=l+r>>1,res=-INF;
	if(ql<=mid) res=max(res,query(x<<1,l,mid,ql,qr));
	if(qr>mid) res=max(res,query(x<<1|1,mid+1,r,ql,qr));
	return res;
}
void setp(int x,int l,int r,int p,int v){
	if(l==r){tr[x].mx=v;tr[x].lazy=0;return;}
	pushdown(x);
	int mid=l+r>>1;
	if(p<=mid) setp(x<<1,l,mid,p,v);
	else setp(x<<1|1,mid+1,r,p,v);
	pushup(x);
}
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		int l,r,a;
		cin>>l>>r>>a;
		e[r].push_back({l,a});
	}
	build(1,0,n);
	for(int i=1;i<=n;i++){
		int best=query(1,0,n,0,i-1);
		setp(1,0,n,i,best);
		for(auto it:e[i]) change(1,0,n,it.l,i,it.a);
	}
	cout<<max(0LL,query(1,0,n,0,n))<<endl;
	return 0;
}

X - Tower

题目大意

有 \(N\) 个方块,第 \(i\) 个重量 \(w_i\)、强度 \(s_i\)、价值 \(v_i\)。一个方块上方总重量不能超过它的强度,求合法塔的最大价值。

数据范围

\(1\le N\le1000,\ 1\le w_i,s_i\le10^4,\ 1\le v_i\le10^9\)。

思考方向

若两个方块相邻,按照 \(w+s\) 较小的放在上方不会更差,因此先按 \(w+s\) 排序。之后做 01 背包:把当前方块放在已有塔的下面,要求已有塔重量 \(\le s_i\)。

DP 设计

  • 状态定义:\(dp_j\):当前已考虑若干方块,总重量为 \(j\) 的合法塔最大价值。

  • 初始化:\(dp_0=0\),其余为 \(-\infty\)。

  • 状态转移:按排序后顺序处理方块,倒序枚举 \(j\le s_i\),更新 \(dp_{j+w_i}=\max(dp_{j+w_i},dp_j+v_i)\)。

  • 答案:所有 \(dp_j\) 的最大值。

  • 复杂度:时间约 \(O(N\cdot 20000)\),空间 \(O(20000)\)。

AC 代码

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
const int M=20005;
const long long INF=4000000000000000000LL;
struct node{int w,s,v;};
int n,dp[M];
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cin>>n;
	vector<node> a(n);
	for(auto &x:a) cin>>x.w>>x.s>>x.v;
	sort(a.begin(),a.end(),[](node x,node y){return x.w+x.s<y.w+y.s;});
	for(int i=1;i<M;i++) dp[i]=-INF;
	dp[0]=0;
	for(auto x:a){
		for(int j=x.s;j>=0;j--){
			if(dp[j]<0) continue;
			dp[j+x.w]=max(dp[j+x.w],dp[j]+x.v);
		}
	}
	int ans=0;
	for(int i=0;i<M;i++) ans=max(ans,dp[i]);
	cout<<ans<<endl;
	return 0;
}

Y - Grid 2

题目大意

在 \(H\times W\) 网格中,从左上走到右下,只能向右或向下,有 \(N\) 个障碍点不可经过,求路径数。

数据范围

\(1\le H,W\le10^5,\ 0\le N\le3000\)。

思考方向

网格大但障碍少。把所有障碍和终点排序,\(dp_i\) 表示第一次遇到这些关键点中的第 \(i\) 个点就是它的方案数。总路径数减去经过更早障碍点的方案。

DP 设计

  • 状态定义:\(dp_i\):从 \((1,1)\) 到关键点 \(i\),且不经过更早障碍点的路径数。

  • 初始化:预处理阶乘和逆元,用组合数 \(C(dx+dy,dx)\) 计算矩形路径数。

  • 状态转移:\(dp_i=C(x_i+y_i-2,x_i-1)-\sum_{j<i,x_j\le x_i,y_j\le y_i}dp_jC(x_i-x_j+y_i-y_j,x_i-x_j)\)。

  • 答案:把终点作为最后一个关键点,输出它的 \(dp\)。

  • 复杂度:时间 \(O(N^2)\),空间 \(O(N+H+W)\)。

AC 代码

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
const int mod=1e9+7;
const int N=3005;
const int M=200005;
struct node{int x,y;};
int h,w,n,fac[M],ifac[M],dp[N];
int ksm(int a,int b){
	int res=1;
	while(b){
		if(b&1) res=res*a%mod;
		a=a*a%mod;b>>=1;
	}
	return res;
}
int C(int n,int m){
	if(n<0||m<0||m>n) return 0;
	return fac[n]*ifac[m]%mod*ifac[n-m]%mod;
}
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cin>>h>>w>>n;
	vector<node> p(n+1);
	for(int i=0;i<n;i++) cin>>p[i].x>>p[i].y;
	p[n]={h,w};
	sort(p.begin(),p.end(),[](node a,node b){return a.x==b.x?a.y<b.y:a.x<b.x;});
	int lim=h+w;
	fac[0]=1;
	for(int i=1;i<=lim;i++) fac[i]=fac[i-1]*i%mod;
	ifac[lim]=ksm(fac[lim],mod-2);
	for(int i=lim;i>=1;i--) ifac[i-1]=ifac[i]*i%mod;
	for(int i=0;i<=n;i++){
		dp[i]=C(p[i].x+p[i].y-2,p[i].x-1);
		for(int j=0;j<i;j++){
			if(p[j].x<=p[i].x&&p[j].y<=p[i].y){
				int ways=C(p[i].x-p[j].x+p[i].y-p[j].y,p[i].x-p[j].x);
				dp[i]=(dp[i]-dp[j]*ways)%mod;
			}
		}
		dp[i]=(dp[i]+mod)%mod;
	}
	cout<<dp[n]<<endl;
	return 0;
}

Z - Frog 3

题目大意

有 \(N\) 个高度严格递增的石头,青蛙可从 \(i\) 跳到任意 \(j>i\),代价为 \((h_j-h_i)^2+C\),求到达 \(N\) 的最小代价。

数据范围

\(2\le N\le2\times10^5,\ 1\le C\le10^{12},\ 1\le h_1<\cdots<h_N\le10^6\)。

思考方向

朴素转移 \(dp_i=\min_{j<i}{dp_j+(h_i-h_j)^2+C}\) 是 \(O(N^2)\)。拆开式子:
\(dp_i=h_i2+C+\min_{j<i}{(-2h_j)h_i+dp_j+h_j2}\)。
每个 \(j\) 是一条直线,在 \(x=h_i\) 处查询最小值,用 Li Chao 线段树维护。

DP 设计

  • 状态定义:\(dp_i\):到达第 \(i\) 个石头的最小代价。每个已处理 \(j\) 加入直线 \(y=(-2h_j)x+(dp_j+h_j^2)\)。

  • 初始化:\(dp_1=0\),先加入第 \(1\) 条直线。

  • 状态转移:\(dp_i=h_i^2+C+query(h_i)\),再加入第 \(i\) 条直线。

  • 答案:输出 \(dp_N\)。

  • 复杂度:时间 \(O(N\log N)\),空间 \(O(N)\)。

AC 代码

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
const long long INF=4000000000000000000LL;
const int N=2e5+5;
int n,C,h[N],dp[N],xs[N];
struct line{int k,b;};
line tr[N*4];
int f(line a,int x){return a.k*x+a.b;}
void init(int x,int l,int r){
	tr[x]={0,INF};
	if(l==r) return;
	int mid=l+r>>1;
	init(x<<1,l,mid);init(x<<1|1,mid+1,r);
}
void insert(int x,int l,int r,line nw){
	int mid=l+r>>1;
	int xl=xs[l],xm=xs[mid],xr=xs[r];
	line cur=tr[x];
	if(f(nw,xm)<f(cur,xm)) swap(nw,tr[x]),cur=tr[x];
	if(l==r) return;
	if(f(nw,xl)<f(cur,xl)) insert(x<<1,l,mid,nw);
	else if(f(nw,xr)<f(cur,xr)) insert(x<<1|1,mid+1,r,nw);
}
int query(int x,int l,int r,int p){
	int res=f(tr[x],xs[p]);
	if(l==r) return res;
	int mid=l+r>>1;
	if(p<=mid) res=min(res,query(x<<1,l,mid,p));
	else res=min(res,query(x<<1|1,mid+1,r,p));
	return res;
}
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cin>>n>>C;
	for(int i=1;i<=n;i++) cin>>h[i],xs[i]=h[i];
	init(1,1,n);
	dp[1]=0;
	insert(1,1,n,{-2*h[1],dp[1]+h[1]*h[1]});
	for(int i=2;i<=n;i++){
		dp[i]=h[i]*h[i]+C+query(1,1,n,i);
		insert(1,1,n,{-2*h[i],dp[i]+h[i]*h[i]});
	}
	cout<<dp[n]<<endl;
	return 0;
}

posted @ 2026-05-05 11:25  艾拉别哭  阅读(41)  评论(0)    收藏  举报