NOIP 二十年 DP 训练总结

2005 至 2024 NOIP 动态规划真题

Posted by TH911 on July 25, 2025

持续更新中,对于较为简单的题目,会直接给出题解。对于较为复杂的题目,会给出核心摘要详细题解的链接

题目主要来源于[此处](https://www.luogu.com.cn/problem/list?type=luogu&page=1&tag=3,129,139,141,144,152,323,435,443,444,464 82,83 22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,58,59,60),以及一些没有 DP 标签但自认为与 DP 有较深的关系的题目。

NOIP2005

[NOIP 2005 普及组] 采药

经典 01 背包问题。

参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; int n,m,w[1001],c[1001],f[1001][1001]; int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ cin>>n>>m; for(int i=1;i<=m;i++){ cin>>w[i]>>c[i]; } for(int i=1;i<=m;i++){ for(int j=1;j<=n;j++){ f[i][j]=f[i-1][j]; if(j>=w[i])f[i][j]=max(f[i][j],f[i-1][j-w[i]]+c[i]); } } cout<<f[m][n]<<endl; /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> ## [[NOIP 2005 提高组] 过河](https://www.luogu.com.cn/problem/P1052) 首先不难设计朴素 DP:设 $\textit{dp}_{i}$ 表示跳到坐标 $i$ 时的最小石子数量。 记 $\textit{flag}_{i}$ 表示坐标 $i$ 是否有石子,有: $$ \textit{dp}_i=\min_{j=i-t}^{i-s}\textit{dp}_j+\textit{flag}_i $$ 注意到 $L\leq10^9$,会 MLE。因此考虑优化。发现 $1\leq s,t\leq10$,也许有一些性质可以利用。 令两个石子之间的距离为 $x$。 则若 $x>t(t-1)$,则 $x\iff t(t-1)$。由此可以 DP。
证明

不妨设步数仅有两种:$p,p+1$。

设 $p$ 走了 $x$ 步,$p+1$ 走了 $y$ 步。

设最终走到的点为 $z$,则有:

$$ px+(p+1)y=n $$

因为 $\gcd(p,p+1)=1$,由裴蜀定理,上述方程一定有整数解

当 $0\leq x\leq p$ 时,有:

$$ y=\dfrac{m-px}{p+1}\geq\dfrac{m-p^2}{p+1}\geq\dfrac{p(p+1)-px-1}{p+1}\geq0 $$

因此,当 $m\geq p(p+1)$ 时,$p(p+1)$ 之后的点一定能走到。

参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; int l,s,t,m,f[10001],a[10001],flag[10001]; int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ scanf("%d",&l); scanf("%d%d%d",&s,&t,&m); for(int i=1;i<=m;i++)scanf("%d",a+i); if(s==t){ int cnt=0; for(int i=1;i<=m;i++)cnt+=((a[i]%s)==0); printf("%d\n",cnt); return 0; }sort(a+1,a+m+1); int pl=min(l-a[m],100); for(int i=m;i>=1;i--)a[i]=min(a[i]-a[i-1],90); l=0; for(int i=1;i<=m;i++){ l+=a[i]; flag[l]=true; }l+=pl; for(int i=1;i<=l+t;i++){ f[i]=2147483646; for(int j=s;j<=t;j++){ if(i>=j)f[i]=min(f[i],f[i-j]+flag[i]); } } int Min=2147483647; for(int i=l;i<=l+t;i++)Min=min(Min,f[i]); printf("%d",Min); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> # NOIP2006 ## [[NOIP 2006 普及组] 开心的金明](https://www.luogu.com.cn/problem/P1060) 经典完全背包问题。
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; //v:价格,p:单个乘积,f:Dp数组 int n,m,v[25],p[25],f[25][30000]; int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ cin>>n>>m; for(int i=1;i<=m;i++){ cin>>v[i]>>p[i]; p[i]*=v[i]; } for(int i=1;i<=m;i++){ for(int j=1;j<=n;j++){ f[i][j]=f[i-1][j]; if(j>v[i])f[i][j]=max(f[i][j],f[i-1][j-v[i]]+p[i]); } } cout<<f[m][n]<<endl; /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> ## [[NOIP 2006 提高组] 能量项链](https://www.luogu.com.cn/problem/P1063) 记 $L(i),R(i)$ 分别表示 $i$ 的前驱/后继。 记 $\textit{dp}_{l,r}$ 表示珠子 $l,l+1,\cdots,r$ 按照某种顺序合并的最大能量,则有: $$ \textit{dp}_{l,r}=\max_{i=l}^r\left(\textit{dp}_{l,i}+a_la_{R(i)}a_{R(r)}+\textit{dp}_{i,r}\right) $$
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; int n,a[101],s[101][101]; int L(int x){ if(x==1)return n; return x-1; } int R(int x){ if(x==n)return 1; return x+1; }//[l,r]最大能量 int f(int l,int r){ if(s[l][r])return s[l][r]; int cnt=0; for(int i=l;i!=r;i=R(i)){ cnt=max(cnt, f(l,i) + f(R(i),r) + a[l] * a[R(i)] * a[R(r)] ); }return s[l][r]=cnt; } int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ scanf("%d",&n); for(int i=1;i<=n;i++)scanf("%d",a+i); int ans=0; for(int i=1;i<=n;i++)ans=max(ans,f(i,L(i)));//环 printf("%d\n",ans); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> ## [[NOIP 2006 提高组] 金明的预算方案](https://www.luogu.com.cn/problem/P1064) 发现主件至多两个附件,因此可以分类讨论。 考虑 DP,设 $\textit{dp}_{i,j}$ 表示前 $i$ 个物品,花费为 $j$ 的答案。 附件的 DP 可以转移到主件上进行,一个主件的状态有: * 只有主件。 * 主件和第一个附件。 * 主件和第二个附件。 * 主件和两个附件。 依此转移即可。
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; constexpr const int N=3.2e4,M=60; struct object{ int v,w; pair<int,int>child; bool main; }a[M+1]; int n,m,dp[M+1][N+1]; int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); cin>>n>>m; for(int i=1;i<=m;i++){ int v,p,q; cin>>v>>p>>q; a[i].v=v; a[i].w=p; if(q){ if(!a[q].child.first){ a[q].child.first=i; }else{ a[q].child.second=i; } a[i].main=false; }else{ a[i].main=true; } } for(int i=1;i<=m;i++){ for(int j=1;j<=n;j++){ dp[i][j]=dp[i-1][j]; int &first=a[i].child.first,&second=a[i].child.second; if(!a[i].main){ continue; } if(j>=a[i].v){ dp[i][j]=max(dp[i][j],dp[i-1][j-a[i].v] + a[i].v*a[i].w); } if(j>=a[i].v+a[first].v){ dp[i][j]=max(dp[i][j],dp[i-1][j-a[i].v-a[first].v] + a[i].v*a[i].w + a[first].v*a[first].w); } if(second){ if(j>=a[i].v+a[second].v){ dp[i][j]=max(dp[i][j],dp[i-1][j-a[i].v-a[second].v] + a[i].v*a[i].w + a[second].v*a[second].w); } if(j>=a[i].v+a[first].v+a[second].v){ dp[i][j]=max(dp[i][j], dp[i-1][j-a[i].v-a[first].v-a[second].v] + a[i].v*a[i].w + a[first].v*a[first].w + a[second].v*a[second].w ); } } } } cout<<dp[m][n]<<'\n'; cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> # NOIP2007 ## [[NOIP 2007 普及组] 守望者的逃离](https://www.luogu.com.cn/problem/P1095) 贪心即可。
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; long long m,s,t,run,fly; int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ cin>>m>>s>>t; for(int i=1;i<=t;i++){ run+=17;//跑步17m/s if(m>=10){ m-=10; fly+=60; }else m+=4; run=max(run,fly); if(run>=s){ cout<<"Yes"<<endl<<i<<endl; return 0; } }cout<<"No"<<endl<<run<<endl; /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> ## [[NOIP 2007 提高组] 矩阵取数游戏](https://www.luogu.com.cn/problem/P1005) 首先需要理解题意: * 不同行之间是独立的。 * 设计一种方式,使得行内得分最大。 * 求所有行的得分之和。 考虑单独一行如何做。设 $\textit{dp}_{l,r}$ 表示取数取走 $a_l,a_{l+1},\cdots,a_r$ 时的最大得分。 则有: $$ \textit{dp}_{l,r}=2\max\left(\textit{dp}_{l+1,r}+a_l,\textit{dp}_{l,r-1}+a_r\right) $$ 考虑第 $i$ 次取 $x$,$x$ 的贡献是 $2^ix$,即上式。因为每次乘 $2$,里面的数便相当于乘上 $2^{i'}$。
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; typedef __int128 lll; constexpr const int N=80,M=80; int n,m,a[N+1][M+1]; lll f(int i,int l,int r){ static lll mem[N+1][N+1][N+1]; if(l<1||m<l||r<1||m<r||r<l){ return 0; } lll &ans=mem[i][l][r]; if(ans){ return ans; } return ans=2*max(f(i,l+1,r)+a[i][l],f(i,l,r-1)+a[i][r]); } template void Write(T x){ static char s[101]; int top=0; do{ s[++top]=x%10^'0'; x/=10; }while(x); while(top){ cout<<s[top--]; } } int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); cin>>n>>m; for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ cin>>a[i][j]; } } lll ans=0; for(int i=1;i<=n;i++){ ans+=f(i,1,m); } Write(ans); cout<<'\n'; cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> ## [[NOIP 2007 提高组] 树网的核](https://www.luogu.com.cn/problem/P1099) > **本题难度在于读题**。 这个题目可能由于命题人~~抽风~~,写的非常**晦涩难懂**。 简化一下,可以得到:给定一棵 $n$ 个点的无根树,对于所有的长度小于等于 $s$ 的路径,求出其他点到路径的最大值(偏心距),并求所有这些最大值的最小值。 $\mathcal O\left(n^3\right)$ 枚举即可。 $k$ 至 $i\sim j$ 的路径的距离可以表述为: $$ \dfrac{\textit{dis}_{i,k}+\textit{dis}_{k,j}-\textit{dis}_{i,j}}2 $$ 其中,$\textit{dis}_{x,y}$ 为 $x\sim y$ 的最短路径长度。
证明

考虑 $i\sim j$ 的路径上一点 $l$,使得 $k\sim l$ 是 $k$ 至路径 $i\sim j$ 的接触点。则 $k$ 至 $i\sim j$ 路径的距离为 $\textit{dis}_{l,k}$。

那么注意到 $i\sim k$ 由 $i\sim l$ 和 $l\sim k$ 两部分组成,$j\sim k$ 同理。

则有:

$$ \begin{aligned} \textit{dis}_{i,k}&=\textit{dis}_{i,l}+\textit{dis}_{l,k}\\ \textit{dis}_{j,k}&=\textit{dis}_{j,l}+\textit{dis}_{l,k} \end{aligned} $$

$l$ 是不确定的,因此考虑消除 $l$,并直接求出 $\textit{dis}_{l,k}$。注意到 $\textit{dis}_{i,l}+\textit{dis}_{j,l}=\textit{dis}_{i,j}$,因此可以两式相加。

于是可以得到上式。

参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; constexpr const int N=300; int n,s,dis[N+1][N+1]; int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); memset(dis,0x3f,sizeof(dis)); cin>>n>>s; for(int i=1;i<n;i++){ int u,v,w; cin>>u>>v>>w; dis[u][v]=dis[v][u]=w; } for(int k=1;k<=n;k++){ dis[k][k]=0; for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]); } } } int ans=2147483647; for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ if(dis[i][j]>s){ continue; } int Max=dis[i][1]+dis[j][1]-dis[i][j]; for(int k=2;k<=n;k++){ Max=max(Max,dis[i][k]+dis[j][k]-dis[i][j]); } ans=min(ans,Max>>1); } } cout<<ans<<'\n'; cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> # NOIP2008 ## [[NOIP 2008 普及组] 传球游戏](https://www.luogu.com.cn/problem/P1057) 设计状态 $f_{i,j}$ 表示传球 $i$ 次,传球到 $j$ 的方案数,答案为 $f_{m,1}$。 不考虑边界,则有: $$ f_{i,j}=f_{i-1,j-1}+f_{i-1,j+1} $$ 即从 $j-1,j+1$ 传球到 $j$。
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; typedef long long ll; ll n,m,f[31][31]; ll F(int i,int j){ if(j<1)return f[i][n]; if(j>n)return f[i][1]; return f[i][j]; } int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ scanf("%lld %lld",&n,&m); f[0][1]=1; for(int i=1;i<=m;i++){ for(int j=1;j<=n;j++){ f[i][j]=F(i-1,j-1)+F(i-1,j+1); } }printf("%lld\n",f[m][1]); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> ## [[NOIP 2008 提高组] 传纸条](https://www.luogu.com.cn/problem/P1006) 设 $n$ 行 $m$ 列。 首先,这等效于从 $(1,1)$ 走两条道路到 $(n,m)$,并且一上一下,互不交叉。这也等效于从 $(1,2)$ 走一条到 $(n-1,m)$,从 $(2,1)$ 走一条到 $(n,m-1)$。 注意到 $2\leq n,m\leq50$,因此考虑一个高维 DP。 设 $\textit{dp}_{i_1,j_1,i_2,j_2}$ 表示从 $(1,2)$ 出发走到 $(i_1,j_1)$,从 $(2,1)$ 出发走到 $(i_2,j_2)$ 的最大合法和。 则有: $$ \textit{dp}_{i_1,j_1,i_2,j_2}=\max\left(\textit{dp}_{i_1-1,j_1,i_2-1,j_2},\textit{dp}_{i_1-1,j_1,i_2,j_2-1},\textit{dp}_{i_1,j_1-1,i_2-1,j_2},\textit{dp}_{i_1,j_1-1,i_2,j_2-1}\right)+a_{i_1,j_1}+a_{i_2,j_2} $$ 需要注意的是,为了保证路径不交叉,有: $$ i_1<i_2\\ j_1>j_2 $$ 即时刻保证 $(i_1,j_1)$ 在 $(i_2,j_2)$ 的右上方。
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; constexpr const int N=50,M=50; int n,m,a[N+1][M+1],dp[N+1][M+1][N+1][M+1]; int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); cin>>n>>m; for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ cin>>a[i][j]; } } for(int i1=1;i1<=n;i1++){ for(int j1=1;j1<=m;j1++){ for(int i2=i1+1;i2<=n;i2++){ for(int j2=1;j2<j1;j2++){ dp[i1][j1][i2][j2]=max({ dp[i1-1][j1][i2-1][j2], dp[i1-1][j1][i2][j2-1], dp[i1][j1-1][i2-1][j2], dp[i1][j1-1][i2][j2-1], })+a[i1][j1]+a[i2][j2]; } } } } cout<<dp[n-1][m][n][m-1]<<'\n'; cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> # NOIP2009 ## [[NOIP 2009 普及组] 道路游戏](https://www.luogu.com.cn/problem/P1070) 记 $\textit{cost}_i$ 为在工厂 $i$ 购买机器人的费用,$a_{j,i}$ 为 $i$ 时刻,马路 $(j-1,j)$ 上的金币数。(实际上,是 $(j-1,j)$ 还是 $(j,j+1)$ 并不重要。) 考虑 DP 求解。 设 $\textit{dp}_{i,j,k}$ 表示在第 $i$ 时刻,走到 $j$,走了 $k$ 步的答案。 $k=0$ 则购买新机器人,从 $i-1$ 时刻的所有合法状态中找一个最大的相加即可。 $k\geq1$ 则从 $j-1$ 走一步即可。 则有: $$ \textit{dp}_{i,j,k}= \begin{cases} \max\limits_{j'=1}^n\max\limits_{k'=0}^{\min(i-1,p)-1}\textit{dp}_{i-1,j',k'}-\textit{cost}_j+a_{j,i} &k=0\\ \textit{dp}_{i-1,j-1,k-1}+a_{j,i} &k\neq1 \end{cases} $$ 时间复杂度:$\mathcal O(nmp)$。 使用滚动数组优化即可做到空间复杂度 $\mathcal O(mp)$。
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; constexpr const int N=1000,M=1000,P=M,inf=0x3f3f3f3f; int n,m,p,a[N+1][M+1],cost[N+1]; int dp[2][N+1][P+1]; int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); cin>>n>>m>>p; for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ cin>>a[i][j]; } } for(int i=1;i<=n;i++){ cin>>cost[i]; } bool mode=0; for(int i=1;i<=m;i++){ mode=!mode; int Max; if(i==1){ Max=0; }else{ Max=-inf; } for(int j=1;j<=n;j++){ for(int k=0;k<i-1&&k<p;k++){ Max=max(Max,dp[!mode][j][k]); } } for(int j=1;j<=n;j++){ dp[mode][j][0]=Max-cost[j]+a[j][i]; for(int k=1;k<i&&k<p;k++){ if(j==1){ dp[mode][j][k]=dp[!mode][n][k-1]+a[j][i]; }else{ dp[mode][j][k]=dp[!mode][j-1][k-1]+a[j][i]; } } } } int ans=dp[mode][1][0]; for(int i=1;i<=n;i++){ for(int j=0;j<m&&j<p;j++){ ans=max(ans,dp[mode][i][j]); } } cout<<ans<<'\n'; cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> ## [[NOIP 2009 提高组] 最优贸易](https://www.luogu.com.cn/problem/P1073) 分为买入和卖出两步,考虑**分层图**。 第一层到第二层表示买入,权值为负;第二层到第三层表示卖出,权值为正。样例即: ![001](https://img2024.cnblogs.com/blog/3541769/202508/3541769-20250804141836205-976462243.webp)
图片来源:link
答案即第一层到最后一层的**最长路** ,存在负权,SPFA 即可。 本题 SPFA 的复杂度是可以接受的,因为 $a_i\leq100$,每一个点**至多更新 $100$ 次**,因此 SPFA 的复杂度上界为 $\mathcal O(100n)$。
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; typedef long long ll; constexpr const int N=100000,M=500000,Len=10000000; int n,a[N+1]; vector<pair<int,int> >g[N*3+1]; int SPFA(int s,int t){ queueq; static bool vis[N*3+1]; static int dis[N*3+1]; memset(dis,0xc0,sizeof(dis)); dis[s]=0; vis[s]=true; q.push(s); while(q.size()){ int x=q.front();q.pop(); vis[x]=false; for(auto i:g[x]){ int &v=i.first,&w=i.second; if(dis[x]+w>dis[v]){ dis[v]=dis[x]+w; if(vis[v]){ continue; } vis[v]=true; q.push(v); } } } return dis[t]; } int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); int m; cin>>n>>m; for(int i=1;i<=n;i++){ cin>>a[i]; g[i].push_back({i+n,-a[i]}); g[i+n].push_back({i+2*n,a[i]}); } while(m--){ int x,y,z; cin>>x>>y>>z; g[x].push_back({y,0}); g[x+n].push_back({y+n,0}); g[x+2*n].push_back({y+2*n,0}); if(z==2){ g[y].push_back({x,0}); g[y+n].push_back({x+n,0}); g[y+2*n].push_back({x+2*n,0}); } } cout<<SPFA(1,3*n)<<'\n'; cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> # NOIP2010 ## [[NOIP 2010 提高组] 乌龟棋](https://www.luogu.com.cn/problem/P1541) 注意到,卡片只有 $1,2,3,4$ 四种,并且保证其数量**均不超过 $40$**。 考虑利用一下这个性质,于是就可以设计 DP。 设 $\textit{dp}_{i,j_1,j_2,j_3,j_4}$ 表示使用 $j_1$ 张 $1$,$j_2$ 张 $2$,$j_3$ 张 $3$,$j_4$ 张 $4$,到达 $i$ 时的最大得分。 则有: $$ \textit{dp}_{i,j_1,j_2,j_3,j_4}=\max_{k=i+1}^4\left([j_k\geq1]\textit{dp}_{i-k,j_1,\cdots,j_k-1,\cdots,j_4}\right)+a_i $$ 于是可以做到 $\mathcal O\left(nV^4\right)$ DP 求解,$V$ 为同一卡片出现次数。$j_i$ 的上界即卡片中 $i$ 的出现次数,且满足 $j_1+2j_2+3j_3+4j_4\leq i$。 但是这样空间复杂度也是 $\mathcal O\left(nV^4\right)$ 的,因此可以使用滚动数组优化,得到 $\mathcal O\left(V^4\right)$ 的空间复杂度。 注意 `inline` 的使用,参见[此处](./-/inline)。编译器可能不会自动 `inline`,建议手写。
进一步优化

注意到,必须有 $i=j_1+2j_2+3j_3+4j_4$,状态 $\textit{dp}_{i,j_1,j_2,j_3,j_4}$ 才是合法的。

那么 $i$ 这一维其实没什么用处,因为可以直接枚举 $j_1,j_2,j_3,j_4$ 来确定。

这样是 $\mathcal O\left(V^4\right)$ 的。

参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; constexpr const int N=350,M=120,V=40; int n,m,a[N+1],cnt[V+1],dp[5][V+1][V+1][V+1][V+1]; inline int ask(int x){ x--; x%=5; if(x<0){ x+=5; } return x; } int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); cin>>n>>m; for(int i=1;i<=n;i++){ cin>>a[i]; } for(int i=1;i<=m;i++){ int x; cin>>x; cnt[x]++; } for(int i=1;i<=n;i++){ for(int j1=0;j1<=cnt[1] && j1<=i;j1++){ for(int j2=0;j2<=cnt[2] && j1+2*j2<=i;j2++){ for(int j3=0;j3<=cnt[3] && j1+2*j2+3*j3<=i;j3++){ for(int j4=0;j4<=cnt[4] && j1+2*j2+3*j3+4*j4<=i;j4++){ int Max=0; if(j1>0&&i>1){ Max=max(Max,dp[ask(i-1)][j1-1][j2][j3][j4]); } if(j2>0&&i>2){ Max=max(Max,dp[ask(i-2)][j1][j2-1][j3][j4]); } if(j3>0&&i>3){ Max=max(Max,dp[ask(i-3)][j1][j2][j3-1][j4]); } if(j4>0&&i>4){ Max=max(Max,dp[ask(i-4)][j1][j2][j3][j4-1]); } dp[ask(i)][j1][j2][j3][j4]=Max+a[i]; } } } } } cout<<dp[ask(n)][cnt[1]][cnt[2]][cnt[3]][cnt[4]]<<'\n'; cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> ## [[NOIP 2010 提高组] 引水入城](https://www.luogu.com.cn/problem/P1514) 发现不是很好求,考虑 DP。 容易发现中间都用处不大,重要的是第一行和最后一行,可以设 $\textit{dp}_{i,j}$ 表示处理到(不一定使用了)第一行前 $i$ 个格子,覆盖了最后一行前 $j$ 个格子的答案。 则有边界: $$ \textit{dp}_{i,0}=0 $$ 考虑如何递推。 设 $\textit{edge}_{i,j}$ 表示 $(1,i)$ 建造之后能否覆盖 $(n,j)$。这可以通过 $m$ 次 DFS 求出。同时可以求出是否存在 $(n,j)$ 不能被任何一个 $(1,i)$ 建造后覆盖,这就是**无解**。 有: $$ \textit{dp}_{i,j}=\min\left(\textit{dp}_{i-1,j},\min_{k+1\sim j\text{ 都可以被覆盖}}\textit{dp}_{i-1,k}\right) $$ 形式化地来讲: $$ \textit{dp}_{i,j}=\min\left(\textit{dp}_{i-1,j},\min_{k\in[0,j-1]\land\operatorname{check}(i,k+1,j)}\textit{dp}_{i-1,k}\right) $$ 其中: $$ \operatorname{check}(i,k+1,j)=\left[0\not\in\set{\textit{edge}_{i,k+1},\textit{edge}_{i,k+2},\cdots,\textit{edge}_{i,j}}\right] $$ 在实际代码中,可以 $j-1\sim 0$ 枚举 $k$,使用 `break` 优化即可。
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; constexpr const int N=500,M=500; constexpr const int f[4][2]={ {0,1},{0,-1},{1,0},{-1,0} }; int n,m,a[N+1][M+1],dp[M+1][M+1]; bool edge[N+1][M+1],can[M+1]; bool vis[N+1][M+1]; void dfs(int i,int j,int s){ vis[i][j]=true; if(i==n){ edge[s][j]=can[j]=true; } for(int k=0;k<4;k++){ int ii=i+f[k][0],jj=j+f[k][1]; if(1<=ii&&ii<=n&&1<=jj&&jj<=m && !vis[ii][jj] && a[i][j]>a[ii][jj]){ dfs(ii,jj,s); } } } int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); cin>>n>>m; for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ cin>>a[i][j]; } } for(int i=1;i<=m;i++){ memset(vis,0,sizeof(vis)); dfs(1,i,i); } int cnt=0; for(int i=1;i<=m;i++){ if(!can[i]){ cnt++; } } if(cnt){ cout<<"0\n"<<cnt<<'\n'; cout.flush(); return 0; } memset(dp,0x3f,sizeof(dp)); for(int i=0;i<=m;i++){ dp[i][0]=0; } for(int i=1;i<=m;i++){ for(int j=1;j<=m;j++){ dp[i][j]=dp[i-1][j]; for(int k=j-1;k>=0;k--){ if(!edge[i][k+1]){ break; } dp[i][j]=min(dp[i][j],dp[i-1][k]+1); } } } cout<<"1\n"<<dp[m][m]<<'\n'; cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> # NOIP2011 ## [[NOIP 2011 普及组] 表达式的值](https://www.luogu.com.cn/problem/P1310) 首先要明确两种运算的定义,用 $+$ 表示 `+`(题面中的 $\oplus$),用 $*$ 表示 `*`(题面中的 $\times$)。 则有: | $+$ | $*$ | | :-----: | :-----: | | $0+0=0$ | $0*0=0$ | | $0+1=1$ | $0*1=0$ | | $1+0=1$ | $1*0=0$ | | $1+1=1$ | $1*1=1$ | 对于诸如此类的关于表达式的问题,有两种做法: * 转前/后缀表达式,栈模拟。 * 转「表达式树」,做树上问题。 对于此题,就可以转**表达式树**后进行树上 DP。 所谓表达式树,即以「实际运算的运算符」为节点(即不包括括号)建二叉树。 运算符间存在优先级的问题,一个子树便代表一个运算结果。即,将原表达式拆分为 $A\circ B$ 的形式,$A,B$ 便是 $\circ$ 的左、右子树运算结果。 $\mathcal O\left(n^2\right)$ 的建树是简单的,即 $[l,r]$ 建成一个树时,找 $x$ 使得 $x$ 对应的是一个**括号之外**的运算符。如果有 $+$ 最好,否则 $*$ 次之。这样可以保证 $+,*$ 之间的运算优先级。特别地,若两端都是括号且配对,则直接建 $[l+1,r-1]$。 但是也存在 $\mathcal O(n)$ 的建树方法: * 建笛卡尔树。 * ST 表先 $\mathcal O(n\log n)$ 预处理每一个点的优先级的区间最小值,建树时 $x$ 即 $[l,r]$ 内优先级最小的点的位置。 具体而言,`(` 和 `)` 的优先级为 $+\infty$。对于 `+` 和 `*`,令包裹其括号层数为 $k$,则 `+` 为 $2k$,`*` 为 $2k+1$。 即被括号包裹一定不优。 *** 树形 DP,设 $\textit{dp}_{x,0},\textit{dp}_{x,1}$ 分别表示 $x$ 子树运算结果为 $0,1$ 时的方案数。 根据运算规则 DP 即可。
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; constexpr const int N=100000,P=10007,inf=0x3f3f3f3f; int n; char a[N+1]; struct expTree{ static int root; struct node{ char op; int lChild,rChild; }t[N<<1|1]; void print(int x=root){ if(!x){ return; } print(t[x].lChild); cerr<<t[x].op<<' '; print(t[x].rChild); } struct sparseTable{ int st[N+1][__lg(N+1)+1],rest[N+1][__lg(N+1)+1]; void build(){ int k=0; for(int i=1;i<=n;i++){ if(a[i]=='('){ k++; st[i][0]=inf; }else if(a[i]==')'){ k--; st[i][0]=inf; }else if(a[i]=='+'){ st[i][0]=k<<1; }else{ st[i][0]=k<<1|1; } rest[i][0]=i; } for(int i=1;(1<<i)<=n;i++){ for(int x=1;x+(1<<i)-1<=n;x++){ if(st[x][i-1]<=st[x+(1<<i-1)][i-1]){ st[x][i]=st[x][i-1]; rest[x][i]=rest[x][i-1]; }else{ st[x][i]=st[x+(1<<i-1)][i-1]; rest[x][i]=rest[x+(1<<i-1)][i-1]; } } } } int query(int l,int r){ int s=__lg(r-l+1); if(st[l][s]<st[r-(1<<s)+1][s]){ return rest[l][s]; }else{ return rest[r-(1<<s)+1][s]; } } }st; int match[N+1]; void pre(){ vectors; for(int i=1;i<=n;i++){ if(a[i]=='('){ s.push_back(i); }else if(a[i]==')'){ match[i]=s.back(); match[s.back()]=i; s.pop_back(); } } st.build(); root=build(1,n); } int create(node x){ static int size; t[++size]=x; return size; } int build(int l,int r){ if(r<l){ return create({}); } if(a[l]=='('&&a[r]==')'&&match[l]==r){ return build(l+1,r-1); } int x=st.query(l,r); return create({a[x],build(l,x-1),build(x+1,r)}); } int dp[N+1][2]; int dfs(int x=root){ if(!t[x].op){ dp[x][0]=dp[x][1]=1; return 1; } int &lChild=t[x].lChild,&rChild=t[x].rChild; dfs(lChild);dfs(rChild); if(t[x].op=='+'){ dp[x][0]=dp[lChild][0]*dp[rChild][0]%P; dp[x][1]=(dp[lChild][0]*dp[rChild][1] + dp[lChild][1]*dp[rChild][0] + dp[lChild][1]*dp[rChild][1])%P; }else{ dp[x][0]=(dp[lChild][0]*dp[rChild][1] + dp[lChild][1]*dp[rChild][0] + dp[lChild][0]*dp[rChild][0])%P; dp[x][1]=dp[lChild][1]*dp[rChild][1]%P; } return dp[x][0]; } }t; int expTree::root; int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); cin>>n; for(int i=1;i<=n;i++){ cin>>a[i]; } t.pre(); cout<<t.dfs()<<'\n'; cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> # NOIP2012 ## [[NOIP 2012 普及组] 摆花](https://www.luogu.com.cn/problem/P1077) 一个很容易想到的 DP 状态是设 $\textit{dp}_{i,j}$ 表示前 $i$ 种花摆了 $j$ 盆的合法方案数。 则有: $$ \begin{aligned} \textit{dp}_{i,j}&=\sum_{k=0}^{\min(a_i,k)}\textit{dp}_{i-1,j-k}\\ \textit{dp}_{0,0}&=1 \end{aligned} $$
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; constexpr const int N=100,M=100,P=1e6+7; int n,m,a[N+1],dp[N+1][M+1]; int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); cin>>n>>m; for(int i=1;i<=n;i++){ cin>>a[i]; } dp[0][0]=1; for(int i=1;i<=n;i++){ for(int j=0;j<=m;j++){ for(int k=0;k<=a[i]&&k<=j;k++){ dp[i][j]=(dp[i][j]+dp[i-1][j-k])%P; } } } cout<<dp[n][m]<<'\n'; cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> # NOIP2013 ## [[NOIP 2013 普及组] 小朋友的数字](https://www.luogu.com.cn/problem/P1982) 最大子段和问题。 记数字为 $\textit{num}_i$,特征值为 $a_i$,分数为 $f_i$。 则有: $$ \begin{aligned} a_i&=\max_{1\leq l\leq r\leq i}\sum_{j=l}^r\textit{num}_j\\ &=\max\left(a_{i-1}+\textit{num}_i,a_{i-1}\right)\\ f_i&= \begin{cases} a_1&i=1\\ \max\left(f_{i-1},f_{i-1}+\max\limits_{j=1}^{i-1}a_j\right)&i>1 \end{cases} \end{aligned} $$ 答案为 $\max\limits_{i=1}^nf_i\bmod p$。$\mathcal O(n)$ 计算。
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; typedef __int128 lll; typedef long long ll; constexpr const int N=1e6; int n,P; ll a[N+1]; lll f[N+1]; int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); cin>>n>>P; f[0]=-(1ll<<62); for(int i=1;i<=n;i++){ cin>>a[i]; a[i]=max(a[i-1]+a[i],a[i]); f[i]=max(f[i-1],(lll)a[i]); } lll pl=f[1]+a[1],ans=f[1]; for(int i=2;i<=n;i++){ ans=max(ans,pl); pl=max(pl,pl+f[i]); } cout<<(int)(ans%P)<<'\n'; cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> ## [[NOIP 2013 提高组] 花匠](https://www.luogu.com.cn/problem/P1970) 显然,最后剩下的花的高度 $g_1,g_2,g_3,\cdots,g_m$ 需要满足下列性质任意一条: * $g_1g_3\cdots$ * $g_1>g_2g_4<\cdots$ 那么,可以据此设计 $\textit{dp}_{i,j},j\in\set{0,1}$ 表示留下 $g_{i'}=h_i$ 时最多留下的花的数量,且满足: * $j=0$:$g_{i'-1}>g_{i'}$。 * $j=1$:$g_{i'-1}<g_{i'}$。 则可以写出转移方程: $$ \begin{aligned} \textit{dp}_{i,0}&=\max_{h_j>h_i\land j<i}\textit{dp}_{j,1}+1\\ \textit{dp}_{i,1}&=\max_{h_j<h_i\land j<i}\textit{dp}_{j,0}+1\\ \end{aligned} $$ 只需要使用线段树优化 DP,或者树状数组优化 DP 即可。
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; //typedef (*fp)(int,int); constexpr const int N=2e6,H=1e9; int n,h[N+1]; int dp[N+1][2]; //前缀最大值 struct bitPre{ int t[N+3+1]; int lowbit(int x){ return x&-x; } void set(int x,int k){ x+=2; while(x<=N+3){ k=max(t[x],k); t[x]=k; x+=lowbit(x); } } int query(int x){ x+=2; int ans=0; while(x){ ans=max(ans,t[x]); x-=lowbit(x); } return ans; } }t0; struct bitSuf{ int t[N+3+1]; int lowbit(int x){ return x&-x; } void set(int x,int k){ x+=2; while(x){ k=max(t[x],k); t[x]=k; x-=lowbit(x); } } int query(int x){ x+=2; int ans=0; while(x<=N+3){ ans=max(ans,t[x]); x+=lowbit(x); } return ans; } }t1; void pre(){ static int tmp[N+1]; for(int i=1;i<=n;i++){ tmp[i]=h[i]; } sort(tmp+1,tmp+n+1); int len=unique(tmp+1,tmp+n+1)-tmp-1; for(int i=1;i<=n;i++){ h[i]=lower_bound(tmp+1,tmp+len+1,h[i])-tmp; } } int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); cin>>n; for(int i=1;i<=n;i++){ cin>>h[i]; } pre(); for(int i=1;i<=n;i++){ dp[i][0]=t1.query(h[i]+1)+1; dp[i][1]=t0.query(h[i]-1)+1; t0.set(h[i],dp[i][0]); t1.set(h[i],dp[i][1]); } int ans=-1; for(int i=1;i<=n;i++){ ans=max({ans,dp[i][0],dp[i][1]}); } cout<<ans<<'\n'; cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> # NOIP2014 ## [[NOIP 2014 普及组] 子矩阵](https://www.luogu.com.cn/problem/P2258) 首先注意到数据范围较小,$1\leq n,m\leq16$。 但是考虑到,如果暴力选出 $r$ 行 $c$ 列,仍会超时。当 $n=m=2r=2c=16$ 时,选出的数据量级约为 $1.7\times10^8$,更何况还要计算答案。因此考虑 DP 优化求解。 首先暴力选出 $r$ 行,记为 $\textit{row}_1,\textit{row}_2,\cdots,\textit{row}_r$。 设 $\textit{dp}_{i,j}$ 表示在 $1\sim i$ 列中,恰好选出 $j$ 列,且第 $i$ 列必须选的子矩阵最小分值。 则有: $$ \textit{dp}_{i,j}=\min_{k=j-1}^{i-1}\textit{dp}_{k,j-1}+\operatorname{calc}(k,i) $$ 即枚举在 $1\sim k$ 列中选出 $j-1$ 列,加上第 $k$ 列右边加上 $i$ 列造成的贡献 $\operatorname{calc}(k,i)$。 分步计算第 $i$ 列内部的贡献及与第 $k$ 列的行间贡献,有: $$ \begin{aligned} \operatorname{calc}(x,y)=\sum_{i=1}^{r-1}\vert a_{\textit{row}_i,y}-a_{\textit{row}_{i+1},y}\vert+\sum_{i=1}^r\vert a_{\textit{row}_i,x}-a_{\textit{row}_i,y}\vert \end{aligned} $$ 故,可以 $\mathcal O\left(\dbinom{n}{r}m^2cr\right)$ 计算答案。
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; constexpr const int N=16,M=16; int n,m,r,c,a[N+1][M+1]; int ans=2147483647; //选的行 int row[N+1]; int calc(int x,int y){ int ans=0; for(int i=1;i<r;i++){ ans+=abs(a[row[i]][y]-a[row[i+1]][y]); } for(int i=1;i<=r;i++){ ans+=abs(a[row[i]][x]-a[row[i]][y]); } return ans; } void Dp(){ static int dp[M+1][M+1]; memset(dp,0x3f,sizeof(dp)); for(int i=1;i<=m;i++){ dp[i][1]=0; for(int j=1;j<r;j++){ dp[i][1]+=abs(a[row[j]][i]-a[row[j+1]][i]); } } for(int i=1;i<=m;i++){ for(int j=1;j<=i&&j<=c;j++){ for(int k=j-1;k<i;k++){ dp[i][j]=min(dp[i][j],dp[k][j-1]+calc(k,i)); } } } for(int i=1;i<=m;i++){ ans=min(ans,dp[i][c]); } } void choose(int p,int choosed){ if(choosed==r){ Dp(); return; } if(p>n){ return; } if(n-p+1+choosed<r){ return; } row[choosed+1]=p; choose(p+1,choosed+1); row[choosed+1]=0; choose(p+1,choosed); } int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); cin>>n>>m>>r>>c; for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ cin>>a[i][j]; } } choose(1,0); cout<<ans<<'\n'; cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> ## [[NOIP 2014 提高组] 联合权值](https://www.luogu.com.cn/problem/P1351) $n$ 个点却是 $n-1$ 条边,显然是一棵树,令根节点为 $1$。记 $f_x$ 表示节点 $x$ 的父节点。 有序点对 $(u,v)$ 的距离为 $2$,显然分两种: * $v$ 的父节点 $f_v$ 是 $u$ 的子节点,即 $u=f_{f_v}$;$v=f_{f_u}$。 这只需要 DFS 一遍,DFS 到当前节点 $x$ 时维护 $x$ 的父节点 $f_x$ 和 $x$ 的子节点计算贡献即可。 这一部分总时间复杂度:$\mathcal O(n)$。 * $u,v$ 有共同父节点 $f_u=f_v$。 记 $x=f_u$,$y_1,y_2,\cdots,y_k$ 为 $x$ 的子节点。 则有: $$ \sum_{i=1}^k\sum_{j=1}^k[i\neq j]w_{y_i}w_{y_j}=\sum_{i=1}^kw_i\left(\sum_{j=1}^kw_{y_j}-w_{y_i}\right) $$ 容易发现 $\sum\limits_{j=1}^kw_{y_j}$ 是定值,那么预处理出来,就可以 $\mathcal O(1)$ 计算。 这一部分总时间复杂度:$\mathcal O(n)$。 那么,我们就在 $\mathcal O(n)$ 的时间内做完了这道题。事实上,可以枚举每一个点作为根节点的时候的子节点之间的贡献,只有一种情况,但是本质相同。
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; typedef long long ll; constexpr const int N=2e5,W=10000,P=10007; int n,w[N+1],sum,Max; vectorg[N+1]; void dfs(int x,int fx){ vectorchildW; for(int i:g[x]){ if(i==fx){ continue; } dfs(i,x); Max=max(Max,w[i]*w[fx]); sum=(sum+2*w[i]*w[fx])%P; childW.push_back(w[i]); } if(childW.size()>=2){ //其实可以不用排序,但是我懒 sort(childW.begin(),childW.end(),[](int a,int b){ return a>b; }); Max=max(Max,childW[0]*childW[1]); ll sumW=0; for(int i:childW){ sumW+=i; } for(int i:childW){ sum=(sum+i*(sumW-i))%P; } } } int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); 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); } for(int i=1;i<=n;i++){ cin>>w[i]; } dfs(1,0); cout<<Max<<' '<<sum<<'\n'; cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> ## [[NOIP 2014 提高组] 飞扬的小鸟](https://www.luogu.com.cn/problem/P1941) 设 $\textit{dp}_{i,j}$ 表示走到 $(i,j)$ 的最小点击屏幕数。若 $(i,j)$ 不在游戏范围内或在管道范围内,则默认 $\textit{dp}_{i,j}=+\infty$。 特别地,对于 $1\leq j\leq m$,有 $\textit{dp}_{0,j}=0$。 * 若从 $i-1$ 下降 $y_{i-1}$ 到达 $(i,j)$,有 $\textit{dp}_{i,j}=\textit{dp}_{i-1,j+y_{i-1}}$。 * 若从 $i-1$ 上升,需要分类讨论。设在 $i-1$ 点击了 $k$ 次,上升了 $kx_{i-1}$。 * 若 $j<m$,有 $\textit{dp}_{i,j}=\textit{dp}_{i-1,j-kx_{i-1}}+1$。 * 否则若 $j=m$,有 $\textit{dp}_{i,m}=\min\limits_{j'=m-kx_{i-1}}^m\textit{dp}_{i-1,j'}+1$。 进行一些优化,即可 $\mathcal O(nm)$ 计算。
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; typedef long long ll; constexpr const int MAXN=10001,MAXM=1001; constexpr const ll INF=1ll<<62; typedef long long ll; int read() { int ret = 0; char c = getchar(); while (c < '0' || c > '9') c = getchar(); while (c >= '0' && c <= '9') ret = ret * 10 + c - '0', c = getchar(); return ret; } int N, M, K, X[MAXN], Y[MAXN], L[MAXN], H[MAXN]; ll dp[MAXN][MAXM], ans[MAXN]; bool W[MAXN]; int main(){ // freopen("test.in","r",stdin); // freopen("test.out","w",stdout); N = read(), M = read(), K = read(); int i, j, x; for (i = 0; i < N; ++i) X[i] = read(), Y[i] = read(); for (i = 1; i <= K; ++i) x = read(), L[x] = read(), H[x] = read(), W[x] = 1; for (i = 0; i <= M; ++i) dp[0][i] = 0; for (i = 1; i <= N; ++i) dp[i][0] = INF; for (i = 1; i <= N; ++i) { ans[i] = INF; for (j = 1; j <= M; ++j) dp[i][j] = INF; for (j = X[i - 1] + 1; j < M; ++j) dp[i][j] = min(dp[i][j], min(dp[i - 1][j - X[i - 1]], dp[i][j - X[i - 1]]) + 1); for (j = M - X[i - 1]; j <= M; ++j) dp[i][M] = min(dp[i][M], min(dp[i - 1][j], dp[i][j]) + 1); for (j = 1; j + Y[i - 1] <= M; ++j) dp[i][j] = min(dp[i][j], dp[i - 1][j + Y[i - 1]]); if (W[i]) { for (j = 1; j <= L[i]; ++j) dp[i][j] = INF; for (j = H[i]; j <= M; ++j) dp[i][j] = INF; } for (j = 1; j <= M; ++j) ans[i] = min(ans[i], dp[i][j]); if (ans[i] == INF) { ll ac = 0; for (j = 1; j < i; ++j) if (W[j]) ++ac; printf("0\n%lld\n", ac); return 0; } } printf("1\n%lld\n", ans[N]); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> # NOIP2015 ## [[NOIP 2015 提高组] 子串](https://www.luogu.com.cn/problem/P2679) 求方案数,考虑 DP。 DP 状态显然需要记录匹配到 $A,B$ 中的位置。同时,为了维护子串数量和字符状态,可以设计 $\textit{dp}_{i,j,k,l},l\in\set{0,1}$,表示用 $A_1,A_2,\cdots,A_i$ 中的 $k$ 个子串,匹配了 $B_1,B_2,\cdots,B_j$,$A_i$ 选与不选的方案数。 则有: $$ \begin{aligned} \textit{dp}_{i,j,k,0}&=\textit{dp}_{i-1,j,k,0}+\textit{dp}_{i-1,j,k,1}\\ \textit{dp}_{i,j,k,1}&= \begin{cases} \textit{dp}_{i-1,j-1,k,1}+\left(\textit{dp}_{i-1,j-1,k-1,0}+\textit{dp}_{i-1,j-1,k-1,1}\right)&A_i=B_j\\ 0&A_i\neq B_j \end{cases} \end{aligned} $$
参考代码

滚动数组优化即可避免 MLE。

```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; constexpr const int N=1000,M=200,K=200,P=1000000007; int n,m,k; char a[N+1],b[M+1]; int f[2][M+1][K+1][2]; int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ scanf("%d %d %d",&n,&m,&k); scanf("%s%s",a+1,b+1); f[0][0][0][0]=f[1][0][0][0]=1; bool mode=0; for(int i=1;i<=n;i++){ mode=!mode; for(int j=1;j<=m;j++){ for(int kk=1;kk<=k;kk++){ if(a[i]==b[j]){ f[mode][j][kk][0]=(f[!mode][j][kk][0]+f[!mode][j][kk][1])%P; f[mode][j][kk][1]=(1ll*f[!mode][j-1][kk][1]+f[!mode][j-1][kk-1][0]+f[!mode][j-1][kk-1][1])%P; }else{ f[mode][j][kk][0]=(f[!mode][j][kk][0]+f[!mode][j][kk][1])%P; f[mode][j][kk][1]=0; } } } } printf("%d\n",(f[mode][m][k][0]+f[mode][m][k][1])%P); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> # NOIP2016 ## [[NOIP 2016 提高组] 换教室](https://www.luogu.com.cn/problem/P1850) 设 $dp_{i,j,k\in\lbrace0,1\rbrace }$ 表示处理到第 $i$ 个时间段,申请换了(不是实际换了几个) $j$ 个教室,第 $i$ 个教室是否更换的答案。 令 $a_{x,y}$ 表示教室 $x$ 到教室 $y$ 的最短路长度,有: $$ \begin{aligned} dp_{i,0,0}&=dp_{i-1,0,0}+a_{c_{i-1},c_i}\\ dp_{i,j,0}&=\min \begin{cases} dp_{i-1,j,0}+a_{c_{i-1},c_i}\\ k_{i-1}(dp_{i-1,j,1}+a_{d_{i-1},c_i})+(1-k_{i-1})(dp_{i-1,j,1}+a_{c_{i-1},c_i}) \end{cases}\\ &=\min \begin{cases} dp_{i-1,j,0}+a_{c_{i-1},c_i}\\ dp_{i-1,j,1}+a_{d_{i-1},c_i}\cdot k_{i-1}+a_{c_{i-1},c_i}(1-k_{i-1}) \end{cases} \\ dp_{i,j,1}&=\min \begin{cases} dp_{i-1,j-1,0}+a_{c_{i-1},d_i}\cdot k_i+a_{c_{i-1},c_i}(1-k_i)\\ dp_{i-1,j-1,1}+a_{d_{i-1},d_i}\cdot k_{i-1}\cdot k_i+a_{d_{i-1},c_i}\cdot k_{i-1}(1-k_i)+a_{c_{i-1},d_i}(1-k_{i-1})k_i+a_{d_{i-1},d_i}(1-k_{i-1})(1-k_i) \end{cases} \end{aligned} $$ DP 边界为 $dp_{1,0,0}=dp_{1,1,1}=0$。 [详细题解参见此处](./-/P1850)。
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; constexpr const int N=2000,M=2000,V=300; constexpr const double Max=1e18; int n,m,v,e,c[N+1],d[N+1],a[V+1][V+1]; double k[N+1],dp[N+1][M+1][2]; template T min(T a,T b,T c){ return (a<b?(a<c?a:c):(b<c?b:c)); } int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); cin>>n>>m>>v>>e; for(int i=1;i<=n;i++){ cin>>c[i]; } for(int i=1;i<=n;i++){ cin>>d[i]; } for(int i=1;i<=n;i++){ cin>>k[i]; } memset(a,0x3f,sizeof(a)); for(int i=1;i<=e;i++){ int u,v,w; cin>>u>>v>>w; a[u][v]=a[v][u]=min(a[u][v],w); } for(int k=1;k<=v;k++){ for(int i=1;i<=v;i++){ for(int j=1;j<=v;j++){ a[i][j]=min(a[i][j],a[i][k]+a[k][j]); } } } for(int i=0;i<=v;i++){ a[i][i]=0; } for(int i=0;i<=n;i++){ for(int j=0;j<=m;j++){ dp[i][j][0]=dp[i][j][1]=Max; } } dp[1][0][0]=dp[1][1][1]=0; for(int i=2;i<=n;i++){ dp[i][0][0]=dp[i-1][0][0]+a[c[i-1]][c[i]]; for(int j=1;j<=i&&j<=m;j++){ dp[i][j][0]=min( dp[i-1][j][0]+a[c[i-1]][c[i]], dp[i-1][j][1]+a[d[i-1]][c[i]]*k[i-1]+a[c[i-1]][c[i]]*(1-k[i-1]) ); dp[i][j][1]=min( dp[i-1][j-1][0]+a[c[i-1]][d[i]]*k[i]+a[c[i-1]][c[i]]*(1-k[i]), dp[i-1][j-1][1]+a[d[i-1]][d[i]]*k[i-1]*k[i]+a[d[i-1]][c[i]]*k[i-1]*(1-k[i])+a[c[i-1]][d[i]]*(1-k[i-1])*k[i]+a[c[i-1]][c[i]]*(1-k[i-1])*(1-k[i]) ); } } double ans=Max; for(int i=0;i<=m;i++){ ans=min(ans,dp[n][i][0],dp[n][i][1]); } cout<<fixed<<setprecision(2)<<ans<<'\n'; /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> ## [[NOIP 2016 提高组] 愤怒的小鸟](https://www.luogu.com.cn/problem/P2831) 注意到 $n\leq18$,因此可以考虑**状压 DP**。 设 $\textit{dp}_s$ 表示射死的猪的集合为 $s$ 时的方案数。 记 $\textit{line}_{i,j}$ 表示射死第 $i,j$ 只猪的合法的抛物线射死的猪的状态。 有: $$ \textit{dp}_{s\cup\textit{line}_{i,j}}\leftarrow\min(\textit{dp}_{s\cup\textit{line}_{i,j}},\textit{dp}_s+1) $$ [详细题解参见此处](./-/P2831)。
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; constexpr const int N=18; constexpr const double eps=1e-6; int n,dp[1<<N|1],line[N+1][N+1]; struct node{ double x,y; }a[N+1]; struct func{ double a,b; }; func calc(node a,node b){ double& x1=a.x,x2=b.x,y1=a.y,y2=b.y; func ans; ans.a=(x2*y1-x1*y2)/(x1*x2*(x1-x2)); ans.b=y1/x1-ans.a*x1; return ans; } int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); int T; cin>>T; while(T--){ int m; cin>>n>>m; for(int i=0;i<n;i++){ cin>>a[i].x>>a[i].y; } memset(line,0,sizeof(line)); for(int i=0;i<n;i++){ line[i][i]|=(1<<i); for(int j=0;j<n;j++){ if(i==j){ continue; } auto pl=calc(a[i],a[j]); if(pl.a>=0){ continue; } for(int k=0;k<n;k++){ if(abs(a[k].y-(pl.a*a[k].x*a[k].x+pl.b*a[k].x))<=eps){ line[i][j]|=(1<<k); } } } } memset(dp,0x3f,sizeof(dp)); dp[0]=0; for(int i=0;i<(1<<n);i++){ for(int j=0;j<n;j++){ for(int k=0;k<n;k++){ dp[i|line[j][k]]=min(dp[i|line[j][k]],dp[i]+1); } } } cout<<dp[(1<<n)-1]<<'\n'; } cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> # NOIP2017 ## [[NOIP 2017 普及组] 跳房子](https://www.luogu.com.cn/problem/P3957) 花费金币越多,灵活性越高,能够到达的格子越多,获得分数越高。 因此答案具有单调性,可以二分花费的金币数 $g$。 设 $\textit{dp}_i$ 为跳到第 $i$ 个格子获得的最大分数,使用单调队列优化 DP 维护转移即可。 转移有: $$ dp_i=s_i+\max_{x_i-d-g\leq x_j\leq x_i+\max(1,d-g)} dp_j $$ [详细题解参见此处](./-/P3957)。
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; typedef long long ll; constexpr const int N=5e5,S=1e5; struct grid{ int x,s; }a[N+1]; int n,d,k; bool noAns(){ ll sum=0; for(int i=1;i<=n;i++){ if(a[i].s>0){ sum+=a[i].s; } } return sum<k; } //跳到i时的最大分数 ll dp[N+1]; bool check(int g){ fill(dp+1,dp+n+1,-1ll*N*S-1); dequeq; q.push_back(0); for(int i=1,p=0;i<=n;i++){ while(a[p].x<=a[i].x-max(1,d-g)){ while(q.size()&&dp[q.back()]<=dp[p]){ q.pop_back(); } q.push_back(p++); } while(q.size()&&(a[q.front()].x<a[i].x-d-g||a[i].x-max(1,d-g)<a[q.front()].x)){ q.pop_front(); } if(q.size()){ dp[i]=dp[q.front()]+a[i].s; if(dp[i]>=k){ return true; } } /*for(int j=0;j<i;j++){ if(a[i].x-d-g<=a[j].x&&a[j].x<=a[i].x-max(1,d-g)){ dp[i]=max(dp[i],dp[j]); } } dp[i]+=a[i].s; */ } return false; } int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); cin>>n>>d>>k; for(int i=1;i<=n;i++){ cin>>a[i].x>>a[i].s; } if(noAns()){ cout<<-1<<'\n'; }else{ int l=0,r=max(a[n].x,d); while(l<r){ int mid=l+r>>1; if(check(mid)){ r=mid; }else{ l=mid+1; } } cout<<r<<'\n'; } cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> ## [[NOIP 2017 提高组] 逛公园](https://www.luogu.com.cn/problem/P3953) 令 $\textit{dis}_i$ 表示从 $1$ 走到 $i$ 的最短路。 考虑设计 DP 求解。设 $\textit{dp}_{u,k}$ 表示从 $1$ 走到 $u$,且总路径长度不超过 $\textit{dis}_u+k$ 的路径数量。 有: $$ \textit{dp}_{u,k}=\sum_{v\in V_u}\textit{dp}_{v,\textit{dis}_u+k-w_{u,v}-\textit{dis}_v} $$ 同时在 DFS 求解过程中,记 $\textit{vis}_{u,k}$ 表示状态 $[u,k]$ 是否出现过,若出现了,则有 $0$ 环。 [详细题解参见此处](./-/P3953)。
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; constexpr const int N=1e5,M=2e5,Kmax=50; struct graph{ struct edge{ int v,r,w; }a[M+1]; int size,h[N+1]; void clear(){ size=0; memset(h,0,sizeof(h)); } void create(int u,int v,int w){ a[++size]={v,h[u],w}; h[u]=size; } }g,rg; int n,m,K,P; int dis[N+1]; int Dijkstra(int s,int t){ memset(dis,0x3f,sizeof(dis)); static bool vis[N+1]; memset(vis,0,sizeof(vis)); dis[s]=0; priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > >q; q.push({dis[s],s}); while(q.size()){ int x=q.top().second;q.pop(); if(vis[x]){ continue; } vis[x]=true; for(int i=g.h[x];i;i=g.a[i].r){ int &v=g.a[i].v; if(vis[v]){ continue; } if(dis[x]+g.a[i].w<dis[v]){ dis[v]=dis[x]+g.a[i].w; q.push({dis[v],v}); } } } return dis[t]; } bool vis[N+1][Kmax+1]; int dp[N+1][Kmax+1]; bool ring0; int f(int u,int k){ if(ring0){ return -1; } if(k<0||K<k){ return 0; } if(vis[u][k]){ ring0=true; return -1; } vis[u][k]=true; if(dp[u][k]){ vis[u][k]=false; return dp[u][k]; } for(int i=rg.h[u];i;i=rg.a[i].r){ int &v=rg.a[i].v,&w=rg.a[i].w; dp[u][k]=(1ll*dp[u][k]+f(v,dis[u]+k-dis[v]-w))%P; } if(u==1&&k==0){ vis[u][k]=false; return 1; } vis[u][k]=false; return dp[u][k]; } int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); int T; cin>>T; while(T--){ g.clear(); rg.clear(); memset(dp,0,sizeof(dp)); memset(vis,0,sizeof(vis)); cin>>n>>m>>K>>P; while(m--){ int u,v,w; cin>>u>>v>>w; g.create(u,v,w); rg.create(v,u,w); } Dijkstra(1,n); ring0=false; f(n,K); if(ring0){ cout<<"-1\n"; }else{ int ans=0; for(int i=0;i<=K;i++){ ans=(1ll*ans+f(n,i))%P; } cout<<ans<<'\n'; } } cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> ## [[NOIP 2017 提高组] 宝藏](https://www.luogu.com.cn/problem/P3959) 因为 $1\leq n\leq12$,因此可以设计指数级算法(一般是搜索或状压 DP),考虑状压 DP。 设 $\textit{dp}_{i,s}$ 表示当前打通的宝藏屋的集合为 $s$,$s$ 中的宝藏屋最大深度为 $i$ 时的最小代价,答案即: $$ \min\limits_{i=0}^n\textit{dp}_{i,\mathbb N\cap[0,n-1]} $$ 枚举 $j$ 的非空真子集 $k$,设 $f_{k,j}$ 表示从状态为 $k$ 打通到 $j$ 的最小边权和,$\textit{can}_{k,j}$ 表示能否从 $k$ 打通到 $j$,有: $$ \textit{dp}_{i,j}=\min_{k\subsetneq j,\textit{can}_{k,j}=1}(\textit{dp}_{i-1,k}+i\cdot f_{k,j}) $$ [详细题解参见此处](./-/P3959)。
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; constexpr const int N=12,inf=0x3f3f3f3f; int n,g[N+1][N+1],dp[N+1][1<<N|1],f[1<<N|1][1<<N|1],can[1<<N|1][1<<N|1],edge[1<<N|1]; string binary(int x){ string ans; while(x){ ans+=(x&1)^'0'; x>>=1; } reverse(ans.begin(),ans.end()); return ans; } main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); int m; cin>>n>>m; memset(g,0x3f,sizeof(g)); while(m--){ int u,v,w; cin>>u>>v>>w; u--,v--; g[u][v]=g[v][u]=min(g[u][v],w); } for(int s=0;s<(1<<n);s++){ edge[s]=s; for(int i=0;i<n;i++){ if(s&(1<<i)){ for(int j=0;j<n;j++){ if(g[i][j]<inf){ edge[s]|=1<<j; } } } } } //从 j 转移到 i 的最小边权和 for(int i=0;i<(1<<n);i++){ for(int j=(i-1)&i;j;j=(j-1)&i){ //j 转移能到 i 的所有点 if((i&edge[j])==i){ can[j][i]=true; static int tmp[N+1]; fill(tmp,tmp+n,inf); for(int k=0;k<n;k++){ if(j&(1<<k)){ for(int l=0;l<n;l++){ if((i&(1<<l)) && !(j&(1<<l))){ tmp[l]=min(tmp[l],g[k][l]); } } } } for(int k=0;k<n;k++){ if((i&(1<<k)) && !(j&(1<<k))){ if(tmp[k]==inf){ can[j][i]=false; break; } f[j][i]+=tmp[k]; } } }else{ can[j][i]=false; } } } memset(dp,0x3f,sizeof(dp)); for(int i=0;i<n;i++){ dp[0][1<<i]=0; } for(int i=1;i<=n;i++){ for(int j=0;j<(1<<n);j++){ for(int k=(j-1)&j;k;k=(k-1)&j){ if(can[k][j]){ dp[i][j]=min(dp[i][j],dp[i-1][k]+i*f[k][j]); } } } } int ans=2147483647; for(int i=0;i<=n;i++){ ans=min(ans,dp[i][(1<<n)-1]); } cout<<ans<<'\n'; cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> # NOIP2018 ## [[NOIP 2018 普及组] 摆渡车](https://www.luogu.com.cn/problem/P5017) 令 $t$ 从小到大有序,设 $dp_{i,j}$ 表示前 $i$ 个人在第 $i$ 个人等待了 $j$ 时刻时全部上车的最短时间。 枚举一个 $k$,表示前 $i+k$ 个人都上车,且第 $i+1\sim i+k$ 个人一起上车。 则可以计算得出第 $i+k$ 个人的等待时间: $$ pl=\max((t_i+j)+m-t_{i+k},0) $$ 可以得到: $$ dp_{i+k,pl}\leftarrow\min\left(dp_{i+k,pl},dp_{i,j}+k(t_{i+k}+pl)-\sum_{l=i+1}^{i+k}t_i\right) $$ 很明显,做一个 $t$ 的前缀和即可单次实现 $\mathcal O(1)$ 转移,总转移复杂度 $\mathcal O(n)$。 [详细题解参见此处](./-/P5017)。
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; constexpr const int N=500,M=100,T=4e6; int n,m,t[N+1],sum[N+1],dp[N+1][M+1]; int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); cin>>n>>m; for(int i=1;i<=n;i++){ cin>>t[i]; } sort(t+1,t+n+1); for(int i=1;i<=n;i++){ sum[i]=sum[i-1]+t[i]; } memset(dp,0x3f,sizeof(dp)); t[0]=-(1<<30); dp[0][0]=0; for(int i=0;i<=n;i++){ // cerr<<"min("<<m-1<<","<<t[i+1]<<"-"<<t[i]<<")="<<min(m-1,t[i+1]-t[i])<<endl; for(int j=0;j<=min(m-1,t[i+1]-t[i]);j++){ // cerr<<"dp on "<<i<<","<<j<<":\n"; // cerr<<"dp["<<i<<"]["<<j<<"]="<<dp[i][j]<<endl; for(int k=1;i+k<=n;k++){ // cerr<<"k="<<k<<endl; int pl=max(t[i]+j+m-t[i+k],0); // cerr<<"pl="<<pl<<endl; dp[i+k][pl]=min(dp[i+k][pl],dp[i][j]+k*(pl+t[i+k])-(sum[i+k]-sum[i])); // cerr<<"dp["<<i+k<<"]["<<pl<<"]="<<dp[i+k][pl]<<endl; } } } int ans=dp[n][0]; for(int i=1;i<m;i++){ ans=min(ans,dp[n][i]); } cout<<ans<<'\n'; cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> ## [[NOIP 2018 提高组] 货币系统](https://www.luogu.com.cn/problem/P5020) 即从 $a_1,a_2,\cdots,a_n$ 中找出最少的 $m$ 个整数,使得这 $m$ 个整数均不能被其他整数表示出来。 因此可以设 $\textit{dp}_{i,j}$ 表示使用 $a_1,a_2,\cdots,a_i$ 表示出 $j$ 的方案数,就是一个完全背包问题。 记 $V=\max\limits_{i=1}^na_i$,答案即: $$ \sum_{i=1}^{V}[\textit{dp}_{n,i}=1] $$
参考代码

使用的是一维数组的完全背包。

```cpp #include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; constexpr const int N=100,V=25000; int n,a[N+1],dp[V+1]; int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ int T; scanf("%d",&T); while(T--){ memset(dp,0,sizeof(dp)); scanf("%d",&n); for(int i=1;i<=n;i++){ scanf("%d",a+i); dp[a[i]]++; } for(int i=1;i<=V;i++){ for(int j=1;j<=n;j++){ if(i>a[j]&&dp[i-a[j]]){ dp[i]++; } } } int ans=0; for(int i=1;i<=n;i++){ if(dp[a[i]]==1){ ans++; } }printf("%d\n",ans); } /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> ## [[NOIP 2018 提高组] 填数游戏](https://www.luogu.com.cn/problem/P5023) 发现 $n,m$ 没有区别,但是 $n\leq8,m\leq10^6$,应该有一些性质可以利用。 考虑打表,于是有: ```cpp int f[9][9]={ {0,0,0,0,0,0,0,0,0}, {0,2,4,8,16,32,64,128,256}, {0,4,12,36,108,324,972,2916,8748}, {0,8,36,112,336,1008,3024,9072,27216}, {0,16,108,336,912,2688,8064,24192,72576}, {0,32,324,1008,2688,7136,21312,63936,191808}, {0,64,972,3024,8064,21312,56768,170112,510336}, {0,128,2916,9072,24192,63936,170112,453504,1360128}, {0,256,8748,27216,72576,191808,510336,1360128,3626752} }; ``` 记 $\textit{ans}_{n,m}$ 为 $n$ 行 $m$ 列的棋盘的答案,钦定 $n\leq m$,则有: * $n=1$ 时,$\textit{ans}_{n,m}=2^m$。 * $n>1$ 时,$\textit{ans}_{n,m}=3\cdot\textit{ans}_{n,m-1}$。
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; typedef long long ll; constexpr const int N=8,M=1e6,P=1e9+7; int n,m; int f[N+1][N+1]={ {0,0,0,0,0,0,0,0,0}, {0,2,4,8,16,32,64,128,256}, {0,4,12,36,108,324,972,2916,8748}, {0,8,36,112,336,1008,3024,9072,27216}, {0,16,108,336,912,2688,8064,24192,72576}, {0,32,324,1008,2688,7136,21312,63936,191808}, {0,64,972,3024,8064,21312,56768,170112,510336}, {0,128,2916,9072,24192,63936,170112,453504,1360128}, {0,256,8748,27216,72576,191808,510336,1360128,3626752} }; int qpow(int base,int n){ int ans=1; while(n){ if(n&1){ ans=1ll*ans*base%P; } base=1ll*base*base%P; n>>=1; } return ans; } int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); cin>>n>>m; if(n>m){ swap(n,m); } if(n==1){ cout<<qpow(2,m)<<'\n'; }else if(m-1<=N){ cout<<f[n][m]<<'\n'; }else{ int ans; if(n!=8){ ans=f[n][n+1]; }else{ ans=10879488; } for(int i=1;i<=m-n-1;i++){ ans=3ll*ans%P; } cout<<ans<<'\n'; } cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> ## [[NOIP 2018 提高组] 保卫王国](https://www.luogu.com.cn/problem/P5024) 题目即求出**最小权覆盖集**,记为 $S$。 记全集为 $U$,**最大权独立集**为 $T$,则有: $$ S=\complement_UT $$ 因此考虑求出 $S$ 即可,使用 [DDP](./-/DDP#ddp-维护树上信息) 可 $\mathcal O\left(n\log ^2n\right)$ 或 $\mathcal O(n\log n)$ 求解。
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; #define int long long //#define DEBUG constexpr const int N=1e5,inf=0x3f3f3f3f3f3f3f3fll; int n,a[N+1]; int f[N+1][2],g[N+1][2]; vectoredge[N+1]; struct Matrix{ int n,m; int a[2][2]; Matrix(int nn=0,int mm=-1){ if(mm==-1){ mm=nn; } n=nn,m=mm; } void unit(){ for(int i=0;i<n;i++){ for(int j=0;j<m;j++){ a[i][j]=-inf; } a[i][i]=0; } } }; Matrix operator*(Matrix A,Matrix B){ Matrix C(A.n,B.m); for(int i=0;i<C.n;i++){ for(int j=0;j<C.m;j++){ C.a[i][j]=-inf; for(int k=0;k<A.m;k++){ C.a[i][j]=max(C.a[i][j],A.a[i][k]+B.a[k][j]); } } } return C; } Matrix& operator*=(Matrix &A,Matrix B){ return A=A*B; } namespace hld{ int size[N+1],father[N+1],son[N+1]; void dfs1(int x,int fx){ father[x]=fx; size[x]=1; for(int i:edge[x]){ if(i==fx){ continue; } dfs1(i,x); size[x]+=size[i]; if(size[i]>size[son[x]]){ son[x]=i; } } } int top[N+1],bottom[N+1],dfn[N+1],rnk[N+1]; void dfs2(int x,int topx){ top[x]=topx; static int cnt; dfn[x]=++cnt; rnk[cnt]=x; if(son[x]){ dfs2(son[x],topx); bottom[x]=bottom[son[x]]; for(int i:edge[x]){ if(i==father[x]||i==son[x]){ continue; } dfs2(i,i); } }else{ bottom[x]=x; } } void build(){ dfs1(1,0); dfs2(1,1); } struct segTree{ struct node{ Matrix value; int l,r; }t[N<<2|1]; Matrix create(int x){ Matrix ans(2); ans.a[0][0]=ans.a[0][1]=g[x][0]; ans.a[1][0]=g[x][1]; ans.a[1][1]=-inf; return ans; } void up(int p){ t[p].value=t[p<<1].value*t[p<<1|1].value; } void build(int p,int l,int r){ t[p].l=l,t[p].r=r; if(l==r){ t[p].value=create(rnk[l]); return; } int mid=l+r>>1; build(p<<1,l,mid); build(p<<1|1,mid+1,r); up(p); } Matrix query(int p,int l,int r){ if(r<l){ Matrix ans(2); ans.unit(); return ans; } if(l<=t[p].l&&t[p].r<=r){ return t[p].value; } Matrix ans(2); ans.unit(); if(l<=t[p<<1].r){ ans*=query(p<<1,l,r); } if(t[p<<1|1].l<=r){ ans*=query(p<<1|1,l,r); } return ans; } void change(int p,int x){ if(t[p].l==t[p].r){ t[p].value=create(rnk[x]); return; } if(x<=t[p<<1].r){ change(p<<1,x); }else{ change(p<<1|1,x); } up(p); } }segTree; void change(int x,int y){ g[x][1]+=-a[x]+y; a[x]=y; segTree.change(1,dfn[x]); x=top[x]; while(x!=1){ Matrix pl(2,1); pl.a[0][0]=0; pl.a[1][0]=a[bottom[x]]; pl=segTree.query(1,dfn[x],dfn[bottom[x]]-1)*pl; int fx0=f[x][0],fx1=f[x][1]; f[x][0]=pl.a[0][0]; f[x][1]=pl.a[1][0]; g[father[x]][0]+=max(f[x][0],f[x][1])-max(fx0,fx1); g[father[x]][1]+=f[x][0]-fx0; segTree.change(1,dfn[father[x]]); x=top[father[x]]; } } int query(){ Matrix pl(2,1); pl.a[0][0]=0; pl.a[1][0]=a[bottom[1]]; if(1<=dfn[bottom[1]]-1){ pl=segTree.query(1,1,dfn[bottom[1]]-1)*pl; } return max(pl.a[0][0],pl.a[1][0]); } } int depth[N+1]; void dfs(int x,int fx){ depth[x]=depth[fx]+1; for(int i:edge[x]){ if(i==fx){ continue; } dfs(i,x); f[x][0]+=max(f[i][0],f[i][1]); f[x][1]+=f[i][0]; } f[x][1]+=a[x]; g[x][0]=f[x][0]-max(f[hld::son[x]][0],f[hld::son[x]][1]); g[x][1]=f[x][1]-f[hld::son[x]][0]; } void pre(){ hld::build(); dfs(1,0); hld::segTree.build(1,1,n); } main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); string type; int m; cin>>n>>m>>type; int sum=0; for(int i=1;i<=n;i++){ cin>>a[i]; sum+=a[i]; } for(int i=1;i<n;i++){ int u,v; cin>>u>>v; edge[u].push_back(v); edge[v].push_back(u); } pre(); while(m--){ int aa,x,bb,y; cin>>aa>>x>>bb>>y; bool noAns=false; if(!x&&!y){ for(int i:edge[aa]){ if(i==bb){ noAns=true; break; } } } if(noAns){ cout<<"-1\n"; continue; } if(depth[aa]<depth[bb]){ swap(aa,bb); swap(x,y); } int ans=0,plA=a[aa],plB=a[bb]; if(x){ hld::change(aa,plA-inf); }else{ hld::change(aa,plA+inf); ans+=inf;//注意到最大权独立集一定会选 inf,那么这里就要减去 inf 得到正确答案 } if(y){ hld::change(bb,plB-inf); }else{ hld::change(bb,plB+inf); ans+=inf; } ans+=sum-hld::query(); cout<<ans<<'\n'; hld::change(aa,plA); hld::change(bb,plB); } cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> # NOIP2019 [众所周知](https://www.noi.cn/xw/2019-08-16/715365.shtml),$2019$ 年是没有 NOIP 的,但是 NOIP 不久就[恢复](https://www.noi.cn/xw/2020-01-21/715520.shtml)了。 # NOIP2020 ## [[NOIP2020] 排水系统](https://www.luogu.com.cn/problem/P7113) 拓扑排序后 DP 统计答案即可。统计答案使用 `__int128` 实现的分数。 [详细题解参见此处](./-/P7113)。
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; typedef unsigned __int128 ll; constexpr const int N=1e5,M=10,D=5; //正分数 ll gcd(ll a,ll b){ while(b){ ll tmp=b; b=a%b; a=tmp; } return a; } ll lcm(ll a,ll b){ return a/gcd(a,b)*b; } struct frac{ ll p,q; frac(){ q=1,p=0; } frac(ll x){ q=1,p=x; } frac(ll pp,ll qq){ p=pp,q=qq; } }; frac operator +(frac a,frac b){ frac c; c.q=lcm(a.q,b.q); c.p=a.p*c.q/a.q+b.p*c.q/b.q; ll pl=gcd(c.p,c.q); c.p/=pl; c.q/=pl; return c; } frac operator +=(frac &a,frac b){ return a=a+b; } frac operator /(frac a,ll b){ a.q*=b; ll pl=gcd(a.p,a.q); a.p/=pl; a.q/=pl; return a; } int n,m; struct node{ int d; int a[D+1]; frac value; }a[N+1]; void topSort(){ static int in[N+1]; for(int i=1;i<=n;i++){ for(int j=1;j<=a[i].d;j++){ in[a[i].a[j]]++; } } queueq; for(int i=1;i<=n;i++){ if(!in[i]){ q.push(i); } } while(q.size()){ int x=q.front();q.pop(); frac add=a[x].value/a[x].d; for(int i=1;i<=a[x].d;i++){ a[a[x].a[i]].value+=add; if(--in[a[x].a[i]]==0){ q.push(a[x].a[i]); } } } } void Write(ll x){ static char s[101]={}; int top=0; do{ s[++top]=x%10^'0'; x/=10; }while(x); while(top){ cout<<s[top--]; } } int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); cin>>n>>m; for(int i=1;i<=n;i++){ cin>>a[i].d; for(int j=1;j<=a[i].d;j++){ cin>>a[i].a[j]; } } for(int i=1;i<=m;i++){ a[i].value=1; } topSort(); for(int i=1;i<=n;i++){ if(!a[i].d){ Write(a[i].value.p); cout<<' '; Write(a[i].value.q); cout<<'\n'; } } /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> # NOIP2021 ## [[NOIP2021] 数列](https://www.luogu.com.cn/problem/P7961) 称二进制最低位为第 $0$ 位(权值为 $2^0$)。 考虑从小到大填入 $a_i$。 设 $dp_{i,j,k,l}$ 表示处理了 $a_1\sim a_i$,$S$ 能进位到的最高位为 $2^j$(实际上最高位也可以不是 $2^j$,之前的 $a_i$ 最大值为 $j-1$),最高位 $2^j$ 有 $k$ 个,已知 $l$ 位为 $1$ 的答案。 则答案为: $$ \sum_{\operatorname{count}(k)+l\leq K}dp_{n,m+1,k,l} $$ 从 $dp_{i,j,k,l}$ 向外转移,有: $$ dp_{i+t,j+1,\left\lfloor\frac{k+t}{2}\right\rfloor,l+(k+t)\bmod 2}\leftarrow dp_{i+t,j+1,\left\lfloor\frac{k+t}{2}\right\rfloor,l+(k+t)\bmod 2}+dp_{i,j,k,l}\cdot v_j^t\cdot \binom{i+t}{t} $$ 边界条件为 $\textit{dp}_{0,0,0,0}=1$。 [详细题解参见此处](./-/P7961)。
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; constexpr const int N=30,M=100,K=N,P=998244353; int n,m,kk,v[M+1]; int dp[N+1][M+1+1][N+1][K+1]; int qpow(int a,int n){ int base=a,ans=1; while(n){ if(n&1){ ans=1ll*ans*base%P; } base=1ll*base*base%P; n>>=1; } return ans; } int C(int n,int m){ static int mem[N+M+1][N+1]; if(n<m){ return 0; } if(m==0||m==n){ return 1; } if(mem[n][m]){ return mem[n][m]; } return mem[n][m]=(1ll*C(n-1,m)+C(n-1,m-1))%P; } int count(int x){ int ans=0; while(x){ if(x&1){ ans++; } x>>=1; } return ans; } int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); cin>>n>>m>>kk; for(int i=0;i<=m;i++){ cin>>v[i]; } dp[0][0][0][0]=1; for(int i=0;i<=n;i++){ for(int j=0;j<=m;j++){ for(int k=0;k<=n;k++){ for(int l=0;l<=kk;l++){ if(dp[i][j][k][l]){ for(int t=0;i+t<=n;t++){ int &pl=dp[i+t][j+1][k+t>>1][l+(k+t&1)]; pl=(1ll*pl+1ll*dp[i][j][k][l]*qpow(v[j],t)%P*C(i+t,t)%P)%P; } } } } } } int ans=0; for(int k=0;k<=n;k++){ for(int l=0;l<=kk;l++){ if(count(k)+l<=kk){ ans=(1ll*ans+dp[n][m+1][k][l])%P; } } } cout<<ans<<'\n'; cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } /* 8 9 4 934258593 150407625 187068439 162292791 219945760 512449588 803393963 983648121 484675481 412407699 642171527 */ ``` </details> ## [[NOIP2021] 方差](https://www.luogu.com.cn/problem/P7962) 操作即**交换差分**,且最终差分**单谷**。因此考虑从单谷两端插入差分值。 记 $d_i=a_{i+1}-a_i,s_i=s_{i-1}+d_i$。 记 $f_{i,j}$ 表示插入了 $d_1,d_2,\cdots,d_{i-1}$,使得 $\sum\limits_{k=1}^{i-1}a_k=j$ 的最小 $\sum\limits_{k=1}^{i-1}a_k^2$。 则有: $$ \begin{aligned} f_{i+1,j+i\cdot d_i}&\leftarrow\min\left(f_{i+1,j+i\cdot d_i},f_{i,j}+2j\cdot d_i+i\cdot d_i^2\right)\\ f_{i+1,j+s_i}&\leftarrow\min\left(f_{i+1,j+s_i},f_{i,j}+s_i^2\right)\\ \end{aligned} $$ 同时注意到测试点 $23\sim25$ $d_i$ 有很多 $0$,因此这一部分跳过。 时间复杂度:$\mathcal O(\min(n,V)nV)$。 [详细题解参见此处](./-/P7962)。
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; typedef long long ll; constexpr const int N=1e4,V=600,Size=5e5; constexpr const ll inf=0x3f3f3f3f3f3f3f3f; int n,a[N+1],diff[N+1],pre[N+1]; ll dp[2][Size+1]; int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); cin>>n; for(int i=1;i<=n;i++){ cin>>a[i]; } for(int i=1;i<n;i++){ diff[i]=a[i+1]-a[i]; } sort(diff+1,diff+n); for(int i=1;i<n;i++){ pre[i]=pre[i-1]+diff[i]; } memset(dp,0x3f,sizeof(dp)); bool mode=0; dp[1][0]=0; for(int i=1;i<n;i++){ if(!diff[i]){ continue; } mode=!mode; memset(dp[!mode],0x3f,sizeof(dp[!mode])); for(int j=0;j<=n*a[n];j++){ if(dp[mode][j]==inf){ continue; } dp[!mode][j+i*diff[i]]=min(dp[!mode][j+i*diff[i]],dp[mode][j] + 2ll*j*diff[i] + 1ll*i*diff[i]*diff[i]); dp[!mode][j+pre[i]]=min(dp[!mode][j+pre[i]],dp[mode][j]+1ll*pre[i]*pre[i]); } } ll ans=inf; for(int i=0;i<=n*a[n];i++){ if(dp[!mode][i]==inf){ continue; } ans=min(ans,n*dp[!mode][i]-1ll*i*i); } cout<<ans<<'\n'; cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> # NOIP2022 ## [[NOIP2022] 种花](https://www.luogu.com.cn/problem/P8865) 记 $\textit{line}_{i,j}$ 表示 $(i,j)$ 右边最长的一段 $0$ 的长度,可以通过 DP $\mathcal O\left(n^2\right)$ 求出。同理,记 $\textit{row}_{i,j}$ 表示 $(i,j)$ 下方最长的一段 $0$ 的数量。 那么,$(i,j)$ 对于 `C` 的答案的贡献为: $$ \sum_{k=i+2}^{i+\textit{row}_{i,j} }\textit{line}_{i,j}\cdot \textit{line}_{k,j}=\textit{line}_{i,j}\sum_{k=i+2}^{i+\textit{row}_{i,j} }\textit{line}_{k,j} $$ 其对于 `F` 的贡献为: $$ \sum_{k=i+2}^{i+\textit{row}_{i,j} }\textit{line}_{i,j}\cdot \textit{line}_{k,j}\cdot \textit{row}_{k,j}=\textit{line}_{i,j}\sum_{k=i+2}^{i+\textit{row}_{i,j} }\textit{line}_{k,j}\cdot \textit{row}_{k,j} $$ 前缀和优化 DP 即可。 [详细题解参见此处](./-/P8865)。
参考代码

写的时候比较懒,写的是 hanglie

```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; constexpr const int N=1000,M=1000,P=998244353; int n,m,Vc,Vf; int a[N+1][M+1],hang[N+1][M+1],lie[N+1][M+1],sumHang[N+1][M+1],sumHangLie[N+1][M+1]; void solve(){ for(int i=1;i<=n;i++){ if(a[i][m]){ hang[i][m]=-1; } for(int j=m-1;1<=j;j--){ if(!a[i][j]){ hang[i][j]=hang[i][j+1]+1; }else{ hang[i][j]=-1; } } } for(int j=1;j<=m;j++){ if(a[n][j]){ lie[n][j]=-1; } for(int i=n-1;1<=i;i--){ if(!a[i][j]){ lie[i][j]=lie[i+1][j]+1; }else{ lie[i][j]=-1; } } } for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ sumHang[i][j]=(1ll*sumHang[i-1][j]+hang[i][j])%P; } } for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ sumHangLie[i][j]=(sumHangLie[i-1][j]+1ll*hang[i][j]*lie[i][j])%P; } } Vc=Vf=0; for(int i=1;i+2<=n;i++){ for(int j=1;j<=m;j++){ if(a[i][j]||a[i+1][j]){ continue; } Vc=(Vc+1ll*hang[i][j]*(sumHang[i+lie[i][j]][j]-sumHang[i+1][j])%P)%P; Vf=(Vf+1ll*hang[i][j]*(sumHangLie[i+lie[i][j]][j]-sumHangLie[i+1][j])%P)%P; // 优化如下代码可得 // for(int k=i+2;k<=i+lie[i][j];k++){ // Vc=(Vc+1ll*hang[i][j]*hang[k][j]%P)%P; // Vf=(Vf+1ll*hang[i][j]*hang[k][j]%P*lie[k][j]%P)%P; // } } } } int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); int T,id; cin>>T>>id; while(T--){ memset(a,0,sizeof(a)); memset(hang,0,sizeof(hang)); memset(lie,0,sizeof(lie)); memset(sumHang,0,sizeof(sumHang)); memset(sumHangLie,0,sizeof(sumHangLie)); int c,f; cin>>n>>m>>c>>f; for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ char ch; cin>>ch; a[i][j]=ch^'0'; } } solve(); if(Vc<0){ Vc+=P; } if(Vf<0){ Vf+=P; } cout<<c*Vc<<' '<<f*Vf<<'\n'; } /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> ## [[NOIP2022] 建造军营](https://www.luogu.com.cn/problem/P8867) 显然,一定需要看守的边为**割边**(桥)。对于其余的边,无论是否看守都可以。 因此可以对原图进行**边双连通分量缩点**。记原图为 $\text{origin}$,新图为 $\text{tree}$。则 $\text{tree}$ 一定是一棵树。因为 $\text{origin}$ 连通,若存在环,则仍然存在边双连通分量,不满足 $\text{tree}$ 的定义。 记 $\operatorname{origin}(i),\operatorname{tree}(i)$ 分别为 $\text{origin},\text{tree}$ 上的 $i$ 号节点。设图 $\text{graph}$,记 $n_{\text{graph}},m_{\text{graph}}$ 分别为 $\text{graph}$ 的点数、边数。 计数问题,考虑在 $\text{tree}$ 上树形 DP。设 $\textit{dp}_i$ 表示 $\text{tree}$ 上 $i$ 子树内选择至少一个节点建造军营的方案数。 记 $e_i$ 表示 $\operatorname{tree}(i)$ 子树内边的总数,这不难 DP 得到;$\textit{cnt}_i$ 表示 $\operatorname{tree}(i)$ 对应 $\text{origin}$ 的多少个节点;$\textit{son}_i$ 表示 $\text{tree}$ 上 $i$ 的子节点集。 容易 DP 得到: $$ \textit{dp}_x=2^{\textit{cnt}_x}\prod_{v\in\textit{son}_x}\left(\textit{dp}_v+2^{e_v+1}\right)-2^{e_x} $$
解释

$\operatorname{tree}(x)$ 上对应的 $\textit{cnt}_x$ 个 $\text{origin}$ 上的点可以随便选,随便选的方案数为 $2^{\textit{cnt}_x}$。

在 $\operatorname{tree}(v)$ 子树内,选点的方案数为 $\textit{dp}_{v,0}$,不选点的方案数为 $2^{e_v+1}$。

同时,减去一个点都不选的方案数 $2^{e_x}$。

*** 设答案为 $2^k\cdot \textit{ans}$ 为答案,其中 $k=m_{\text{origin}}-m_{\text{tree}}=m_{\text{origin}}-n_{\text{tree}}+1$ 为**非割边**数量。 则 $\operatorname{tree}(x)$ 对于 $\textit{ans}$ 的贡献为: $$ 2^{m_\text{tree}-e_x}\left(\textit{dp}_x-\sum_{v\in\textit{son}_x}2^{e_x-e_v-1}\cdot\textit{dp}_v\right) $$
解释

考虑在所选点集的公共 LCA 为 $x$ 时计算贡献。

在 $\operatorname{tree}(x)$ 子树外的部分可以任意看守,方案数 $2^{m_\text{tree}-e_x}$。

LCA 为 $x$,则要么 $x$ 本身被选取,要么两棵及以上的子树内被选取。使用 $\textit{dp}_x$ 减去仅有一个子树被选取的情况即可。

即: $$ \textit{ans}=\sum_{x=1}^{n_\text{tree}}2^{m_\text{tree}-e_x}\left(\textit{dp}_x-\sum_{v\in\textit{son}_x}2^{e_x-e_v-1}\cdot\textit{dp}_v\right) $$
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; constexpr const int N=5e5,P=1e9+7; int qpow(int base,int n){ int ans=1; while(n){ if(n&1){ ans=1ll*ans*base%P; } base=1ll*base*base%P; n>>=1; } return ans; } template struct graph{ int n; vectorg[N+1]; vector& operator [](int x){ return g[x]; } }; graph<pair<int,int>>origin; graphtree; int id[N+1],cnt[N+1]; void Tarjan(int x,int last){ static int dfn[N+1],low[N+1]; static vectors; s.push_back(x); dfn[x]=low[x]=s.size(); for(auto i:origin[x]){ int &v=i.first,&id=i.second; if(id==last){ continue; } if(!dfn[v]){ Tarjan(v,id); low[x]=min(low[x],low[v]); }else{ low[x]=min(low[x],dfn[v]); } } if(dfn[x]==low[x]){ tree.n++; while(s.back()!=x){ id[s.back()]=tree.n; cnt[tree.n]++; s.pop_back(); } id[s.back()]=tree.n; cnt[tree.n]++; s.pop_back(); } } void buildTree(){ Tarjan(1,0); int cnt=0; for(int i=1;i<=origin.n;i++){ for(auto j:origin[i]){ if(id[i]!=id[j.first]){ tree[id[i]].push_back(id[j.first]); } } } for(int i=1;i<=tree.n;i++){ sort(tree[i].begin(),tree[i].end()); tree[i].resize(unique(tree[i].begin(),tree[i].end())-tree[i].begin()); } } void dfs(int x,int fx,int &ans){ static int dp[N+1],e[N+1]; dp[x]=qpow(2,cnt[x]); for(int i:tree[x]){ if(i==fx){ continue; } dfs(i,x,ans); dp[x]=1ll*dp[x]*(dp[i]+qpow(2,e[i]+1))%P; e[x]+=e[i]+1; } dp[x]=(dp[x]-qpow(2,e[x]))%P; int pl=dp[x]; for(int i:tree[x]){ if(i==fx){ continue; } pl=(pl-1ll*dp[i]*qpow(2,e[x]-e[i]-1))%P; } ans=(ans+1ll*pl*qpow(2,tree.n-1-e[x]))%P; } int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); int m; cin>>origin.n>>m; for(int i=1;i<=m;i++){ int u,v; cin>>u>>v; origin[u].push_back({v,i}); origin[v].push_back({u,i}); } buildTree(); int ans=0; dfs(1,0,ans); ans=1ll*ans*qpow(2,m-tree.n+1)%P; if(ans<0){ ans+=P; } cout<<ans<<'\n'; cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> # NOIP2023 ## [[NOIP2023] 双序列拓展](https://www.luogu.com.cn/problem/P9870) 原问题相当于一个 $n\times m$ 的网格,钦定 $f_i<g_i$,则有 $(i,j)$ 染为黑色当且仅当 $x_i<y_j$。求从 $(1,1)$ 能否走到 $(n,m)$。其余节点染为白色。 有解不好考虑,考虑什么情况无解。 无解仅有四种情况: 1. 一行全部为白色,即 $\max\limits_{i=1}^nx_i\geq\max\limits_{i=1}^my_i$。 2. 一列全部为白色,即 $\min\limits_{i=1}^nx_i\geq\min\limits_{i=1}^my_i$。 3. 起点被一个「L 形白色图形」包围,即存在 $(i,j)$,满足 $x_i\geq\max\limits_{k=1}^jy_k\land \min\limits_{k=1}^ix_k\geq y_j$。 4. 终点被一个「L 形白色图形」包围,即存在 $(i,j)$,满足 $x_i\geq\max\limits_{k=j}^my_k\land\min\limits_{k=i}^nx_k\geq y_j$。 有效的 $x_i,y_j$ 均单调递增,指针维护即可。 [详细题解参见此处](./-/P9870)
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; constexpr const int N=5e5,M=5e5; int n,m,x[N+1],y[M+1]; bool check(int x[],int n,int y[],int m){ if(x[1]>=y[1]){ return false; } int maxX=x[1],maxY=y[1],minX=x[1],minY=y[1]; for(int i=1;i<=n;i++){ maxX=max(maxX,x[i]); minX=min(minX,x[i]); } for(int i=1;i<=m;i++){ maxY=max(maxY,y[i]); minY=min(minY,y[i]); } if(maxX>=maxY||minX>=minY){ return false; } int j=1; for(int i=1,preMinX=x[1],j=1,k=1;i<=n;i++){ preMinX=min(preMinX,x[i]); while(j<=m&&y[j]<=x[i]){ j++; } while(k<j&&preMinX<y[k]){ k++; } if(k<j){ return false; } } reverse(x+1,x+n+1); reverse(y+1,y+m+1); j=1; for(int i=1,preMinX=x[1],j=1,k=1;i<=n;i++){ preMinX=min(preMinX,x[i]); while(j<=m&&y[j]<=x[i]){ j++; } while(k<j&&preMinX<y[k]){ k++; } if(k<j){ reverse(x+1,x+n+1); reverse(y+1,y+m+1); return false; } } reverse(x+1,x+n+1); reverse(y+1,y+m+1); return true; } int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); int c,q; cin>>n>>n>>m>>q; for(int i=1;i<=n;i++){ cin>>x[i]; } for(int i=1;i<=m;i++){ cin>>y[i]; } cout<<(check(x,n,y,m)||check(y,m,x,n)); while(q--){ vector<pair<int,int> >kx,ky; int sizeX,sizeY; cin>>sizeX>>sizeY; kx.resize(sizeX);ky.resize(sizeY); #define p first #define v second for(auto &i:kx){ cin>>i.p>>i.v; } for(auto &i:ky){ cin>>i.p>>i.v; } for(auto &i:kx){ swap(x[i.p],i.v); } for(auto &i:ky){ swap(y[i.p],i.v); } cout<<(check(x,n,y,m)||check(y,m,x,n)); for(auto &i:kx){ x[i.p]=i.v; } for(auto &i:ky){ y[i.p]=i.v; } #undef p #undef v } cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> ## [[NOIP2023] 天天爱打卡](https://www.luogu.com.cn/problem/P9871) 设 $\textit{dp}_{i,1},\textit{dp}_{i,0}$ 分别表示第 $i$ 天是否跑步的最大能量值。 记 $[l_i,r_i,w_i]$ 表示 $l_i\sim r_i$ 连续跑步,能量值增加 $w_i$。 设: $$ \displaystyle\operatorname{query}_i(x)=\sum_{j=1}^m[r_j\leq i][l_i\leq x]w_i $$ 则有: $$ \begin{aligned} \textit{dp}_{i,0}&=\max\left(\textit{dp}_{i-1,0},\textit{dp}_{i-1,1}\right)\\ \textit{dp}_{i,1}&=\max_{j=\max(i-k,0)}^{i-1}\left(\textit{dp}_{j,0}+jd+\operatorname{query}_i(j+1)\right)-id \end{aligned} $$ 线段树优化 DP 可以做到 $\mathcal O(n\log n)$ 求解。 决策点为 $l_i-1,r_i$,因此对 $l_1-1,l_2-1,\cdots,l_m-1,r_1-1,r_2-1,\cdots,r_m-1$ 离散化,即可做到 $\mathcal O(m\log m)$ 求解。 [详细题解参见此处](./-/P9871)。
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; typedef long long ll; constexpr const ll inf=0x3f3f3f3f3f3f3f3f; constexpr const int N=1e9,K=N,M=1e5; struct line{ int l,r,w; }a[M+1]; struct segTree{ struct node{ int l,r; ll max,tag; }t[M<<4|1]; inline void up(int p){ t[p].max=max(t[p<<1].max,t[p<<1|1].max); } void build(int p,int l,int r){ t[p].l=l,t[p].r=r; t[p].tag=0; if(l==r){ t[p].max=0; return; } int mid=l+r>>1; build(p<<1,l,mid); build(p<<1|1,mid+1,r); up(p); } inline void down(int p){ if(t[p].tag){ t[p<<1].max+=t[p].tag; t[p<<1].tag+=t[p].tag; t[p<<1|1].max+=t[p].tag; t[p<<1|1].tag+=t[p].tag; t[p].tag=0; } } void add(int p,int l,int r,ll k){ if(l<=t[p].l&&t[p].r<=r){ t[p].tag+=k; t[p].max+=k; return; } down(p); if(l<=t[p<<1].r){ add(p<<1,l,r,k); } if(t[p<<1|1].l<=r){ add(p<<1|1,l,r,k); } up(p); } ll query(int p,int l,int r){ if(l<=t[p].l&&t[p].r<=r){ return t[p].max; } down(p); ll ans=-inf; if(l<=t[p<<1].r){ ans=query(p<<1,l,r); } if(t[p<<1|1].l<=r){ ans=max(ans,query(p<<1|1,l,r)); } return ans; } }t; int n,m,k,d; ll dp[M<<1|1][2]; int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); int c,T; cin>>c>>T; while(T--){ cin>>n>>m>>k>>d; t.build(1,0,m<<1); static int tmp[M<<1|1]; int len=0; for(int i=1;i<=m;i++){ int &l=a[i].l,&r=a[i].r,&w=a[i].w; int x,y; cin>>x>>y>>w; r=x; l=x-y/*+1*/; tmp[++len]=l; tmp[++len]=r; } sort(tmp+1,tmp+len+1); len=unique(tmp+1,tmp+len+1)-tmp-1; for(int i=1;i<=m;i++){ a[i].l=lower_bound(tmp+1,tmp+len+1,a[i].l)-tmp; a[i].r=lower_bound(tmp+1,tmp+len+1,a[i].r)-tmp; } sort(a+1,a+m+1,[](line a,line b){ if(a.r!=b.r){ return a.r<b.r; }else{ return a.l<b.l; } }); for(int i=1,p=1,last=0;i<=len;i++){ dp[i][0]=max(dp[i-1][0],dp[i-1][1]); t.add(1,i-1,i-1,dp[i-1][0] + 1ll*tmp[i-1]*d); while(p<=m&&a[p].r<=i){ t.add(1,0,a[p].l,a[p].w); p++; } while(tmp[last]<tmp[i]-k){ last++; } dp[i][1]=t.query(1,last,i-1)-1ll*tmp[i]*d; } cout<<max(dp[len][0],dp[len][1])<<'\n'; } cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> # NOIP2024 ## [[NOIP2024] 树的遍历](https://www.luogu.com.cn/problem/P11363) 从关键边的角度不好思考,直接考虑生成树。 一棵确定的生成树可能的起始边构成了一条原树上叶节点到叶节点的链,且必须包含至少一条关键边。 一条链确定后,可以确定其产生的生成树数量,树形 DP 统计贡献即可。 [详细题解参见此处](./-/P11363)。
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; constexpr const int N=1e5,K=N-1,P=1e9+7; int n,k,fact[N+1],inv[N+1]; int d[N+1]; vector<pair<int,int> >g[N+1]; bool flag[N+1]; int qpow(int base,int n){ int ans=1; while(n){ if(n&1){ ans=1ll*ans*base%P; } base=1ll*base*base%P; n>>=1; } return ans; } void pre(){ fact[0]=1; for(int i=1;i<=N;i++){ fact[i]=1ll*fact[i-1]*i%P; } inv[N]=qpow(fact[N],P-2); for(int i=N-1;i>=0;i--){ inv[i]=1ll*inv[i+1]*(i+1)%P; } for(int i=1;i<=N;i++){ inv[i]=1ll*inv[i]*fact[i-1]%P; } } int dp[N+1][2]; void dfs(int x,int fx,int &ans){ int pl=0; for(auto i:g[x]){ int &v=i.first,w=flag[i.second]; if(v==fx){ continue; } dfs(v,x,ans); if(w){ dp[v][1]=(dp[v][1]+dp[v][0])%P; dp[v][0]=0; } pl=(pl+1ll*(dp[x][0]+dp[x][1])%P*dp[v][1]+1ll*dp[x][1]*dp[v][0]%P)%P; dp[x][0]=(dp[x][0]+dp[v][0])%P; dp[x][1]=(dp[x][1]+dp[v][1])%P; } ans=(ans+1ll*pl*inv[d[x]-1])%P; if(d[x]==1){ dp[x][0]=(dp[x][0]+1)%P; } dp[x][0]=1ll*dp[x][0]*inv[d[x]-1]%P; dp[x][1]=1ll*dp[x][1]*inv[d[x]-1]%P; } void Start(){ for(int i=1;i<=n;i++){ g[i].resize(0); } memset(d,0,sizeof(d)); memset(flag,0,sizeof(flag)); memset(dp,0,sizeof(dp)); } int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); pre(); int c,T; cin>>c>>T; while(T--){ Start(); cin>>n>>k; for(int i=1;i<n;i++){ int u,v; cin>>u>>v; d[u]++;d[v]++; g[u].push_back({v,i}); g[v].push_back({u,i}); } for(int i=1;i<=k;i++){ int e; cin>>e; flag[e]=true; } if(n==2){ cout<<"1\n"; continue; } int ans=0; for(int i=1;i<=n;i++){ if(d[i]>1){ dfs(i,0,ans); break; } } for(int i=1;i<=n;i++){ ans=1ll*ans*fact[d[i]-1]%P; } cout<<ans<<'\n'; } cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details>