P1020 导弹拦截【最长上升子序列LIS】
- 分别求最长不上升子序列、最长上升子序列
O ( n 2 ) O(n^2) O(n2)算法
#include<iostream>
#include<algorithm>
using namespace std;
int arr[100010],n,dp[100010],lds,cnt,f[100010],lis;
int ldsfind(int x){
int ans=0;
for(int i=1;i<x;i++){
if(arr[i]>=arr[x]&&dp[i]>ans) ans=dp[i];
}
return ans;
}
int lisfind(int x){
int ans=0;
for(int i=1;i<x;i++){
if(arr[i]<arr[x]&&f[i]>ans) ans=f[i];
}
return ans;
}
int main(){
while(cin>>arr[++n]);n--;
dp[1]=1;f[1]=1;
for(int i=2;i<=n;i++){
dp[i]=ldsfind(i)+1;
f[i]=lisfind(i)+1;
}
for(int i=1;i<=n;i++){
lds=max(lds,dp[i]);
lis=max(lis,f[i]);
}
cout<<lds<<endl;
cout<<lis<<endl;
}
O ( n l o g n ) O(nlogn) O(nlogn)算法
- x [ k ] x[k] x[k]为长度为 k k k的上升子序列的最末元素,若有多个长度为 k k k的上升子序列,则记录最小的那个最末元素
- x [ ∗ ] x[*] x[∗]中元素是单调递增的,对 a [ i ] a[i] a[i]:若 a [ i ] > x [ l e n ] a[i]>x[len] a[i]>x[len],那么 x [ + + l e n ] = a [ i ] x[++len] = a[i] x[++len]=a[i];否则,从 x [ 1 ] x[1] x[1]到 x [ l e n ] x[len] x[len]中找到一个 j j j,满足 x [ j − 1 ] < a [ i ] < x [ j ] x[j-1]<a[i]<x[j] x[j−1]<a[i]<x[j],根据 x [ ∗ ] x[*] x[∗]的定义,我们需要更新长度为j的上升子序列的最末元素(使之为最小的)即 x [ j ] = a [ i ] x[j] = a[i] x[j]=a[i];
#include<iostream>
#include<algorithm>
using namespace std;
int arr[100010],len=1,x[100010],n;
int find(int i){
int l=1,r=len,ans=-1;
while(l<=r){
int mid=(l+r)>>1;
if(arr[i]>x[mid]){
ans=mid;
r=mid-1;
}
else l=mid+1;
}
return ans;
}
int find2(int i){
int l=1,r=len,ans=-1;
while(l<=r){
int mid=(l+r)>>1;
if(arr[i]<=x[mid]){
ans=mid;
r=mid-1;
}
else l=mid+1;
}
return ans;
}
int main(){
while(cin>>arr[++n]);n--;
x[1]=arr[1];
for(int i=2;i<=n;i++){
if(arr[i]<=x[len]) x[++len]=arr[i];
else{
x[find(i)]=arr[i];
}
}
cout<<len<<endl;
len=1;
x[1]=arr[1];
for(int i=2;i<=n;i++){
if(arr[i]>x[len]) x[++len]=arr[i];
else{
x[find2(i)]=arr[i];
}
}
cout<<len<<endl;
}
P1439 【模板】最长公共子序列【LCS】
O ( n 2 ) O(n^2) O(n2)算法
#include<iostream>
#include<algorithm>
using namespace std;
int n,a[10010],b[10010],f[10010][10010];
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=n;i++){
cin>>b[i];
}
f[1][0]=f[0][1]=f[0][0]=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(a[i]==b[j]) f[i][j]=f[i-1][j-1]+1;
else{
f[i][j]=max(f[i-1][j],f[i][j-1]);
}
}
}
cout<<f[n][n]<<endl;
}
O ( n l o g n ) O(nlogn) O(nlogn)算法
- 对A数组利用 m p [ x ] = i mp[x]=i mp[x]=i将A变成一个递增的序列
- 对B数组利用A中的映射规则,转化为求B的最长递增子序列问题
#include<iostream>
#include<algorithm>
using namespace std;
int a[100010],b[100010],mp[100010],len=1,x[100010],n;
int find(int i){
int l=1,r=len,ans=-1;
while(l<=r){
int mid=(l+r)>>1;
if(mp[b[i]]<=x[mid]){
ans=mid;
r=mid-1;
}
else l=mid+1;
}
return ans;
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];mp[a[i]]=i;
}
for(int i=1;i<=n;i++){
cin>>b[i];
}
x[1]=mp[b[1]];
for(int i=2;i<=n;i++){
if(mp[b[i]]>x[len]) x[++len]=mp[b[i]];
else{
x[find(i)]=mp[b[i]];
}
}
cout<<len<<endl;
}
P1280 尼克的任务
- f [ i ] f[i] f[i]表示第 i i i个时刻到第 n n n个时刻的值,逆向更新
- b [ i ] b[i] b[i]表示以第 i i i个时刻为起点的所有时间段的统计
- f [ i ] = { f [ i + 1 ] + 1 , b [ i ] = = 0 m a x ( f [ i ] , f [ i + a [ c n t ] . t ] ) , b [ i ] ≠ 0 f[i]=\left\{ \begin{aligned} f[i+1]+1 ,b[i]==0 \\ max(f[i],f[i+a[cnt].t]),b[i]\neq 0 \end{aligned} \right. f[i]={f[i+1]+1,b[i]==0max(f[i],f[i+a[cnt].t]),b[i]=0
#include<iostream>
#include<algorithm>
int n,k;
using namespace std;
const int maxn=10010;
struct node{
int p,t;
bool operator<(const node& n) const{
return p>n.p;
}
}a[maxn];
int b[maxn],cnt=1,f[maxn];
int main(){
cin>>n>>k;
for(int i=1;i<=k;i++){
cin>>a[i].p>>a[i].t;
b[a[i].p]++;
}
sort(a+1,a+k+1);
for(int i=n;i>=1;i--){
if(b[i]==0) f[i]=f[i+1]+1;
else{
for(int j=1;j<=b[i];j++){
f[i]=max(f[i],f[i+a[cnt].t]);
cnt++;
}
}
}
cout<<f[1]<<endl;
}
P2758 编辑距离
- f [ i ] [ j ] f[i][j] f[i][j]表示 A [ 1 − i ] A[1-i] A[1−i]与 B [ 1 − j ] B[1-j] B[1−j]的编辑距离
- f [ i ] [ j ] = m a x { f [ i − 1 ] [ j ] + 1 , A 删 除 一 个 字 符 f [ i ] [ j − 1 ] + 1 , A 删 除 一 个 字 符 f [ i − 1 ] [ j − 1 ] + 1 , A 修 改 一 个 字 符 f[i][j]=max\left\{ \begin{aligned} f[i-1][j]+1,A删除一个字符 \\ f[i][j-1]+1,A删除一个字符 \\ f[i-1][j-1]+1,A修改一个字符 \end{aligned} \right. f[i][j]=max⎩⎪⎨⎪⎧f[i−1][j]+1,A删除一个字符f[i][j−1]+1,A删除一个字符f[i−1][j−1]+1,A修改一个字符
#include<iostream>
#include<string>
#include<algorithm>
using namespace std;
string A,B;
int f[2010][2010];
int main(){
cin>>A>>B;A.insert(0," ");B.insert(0," ");
for(int i=0;i<A.length();i++) f[i][0]=i;
for(int j=0;j<B.length();j++) f[0][j]=j;
for(int i=1;i<A.length();i++){
for(int j=1;j<B.length();j++){
f[i][j]=min(f[i-1][j]+1,min(f[i][j-1]+1,f[i-1][j-1]+(A[i]==B[j]?0:1)));
}
}
cout<<f[A.length()-1][B.length()-1]<<endl;
}
P1040 加分二叉树【区间DP】
- f [ i ] [ j ] f[i][j] f[i][j]表示节点 i i i到节点 j j j的最大加分
- 在区间 [ i , j ] [i,j] [i,j]中枚举树根的位置,状态转移方程为 f [ i ] [ j ] = m a x ( f [ i ] [ k − 1 ] ∗ f [ k + 1 ] [ j ] + f [ k ] [ k ] ) , k ∈ [ l , r ) f[i][j]=max(f[i][k-1]*f[k+1][j]+f[k][k]),k\in[l,r) f[i][j]=max(f[i][k−1]∗f[k+1][j]+f[k][k]),k∈[l,r)
- r o o t [ i ] [ j ] root[i][j] root[i][j]记录从节点 i i i到节点 j j j取得最大加分时的子树的根
#include<iostream>
#include<algorithm>
typedef long long ll;
using namespace std;
int n,root[40][40];
ll f[40][40];
void preorder(ll l,ll r){
if(l<=r){
cout<<root[l][r]<<" ";
preorder(l,root[l][r]-1);
preorder(root[l][r]+1,r);
}
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>f[i][i];
f[i][i-1]=1;//子树不存在,返回1
root[i][i]=i;
}
for(int len=1;len<=n;len++){
for(int l=1;l+len-1<=n;l++){
int r=l+len-1;
for(int k=l;k<r;k++){
if(f[l][r]<f[l][k-1]*f[k+1][r]+f[k][k]){
f[l][r]=f[l][k-1]*f[k+1][r]+f[k][k];
root[l][r]=k;
}
}
}
}
cout<<f[1][n]<<endl;
preorder(1,n);
}
P4933 大师
- f [ i ] [ k ] f[i][k] f[i][k]为以第 i i i个元素结尾的,公差为 k k k的等差子数列的个数
- f [ i ] [ k ] = ∑ j < i f [ j ] [ k ] + 1 ( a [ i ] − a [ j ] = = k ) f[i][k]=\sum_{j<i}f[j][k]+1(a[i]-a[j]==k) f[i][k]=∑j<if[j][k]+1(a[i]−a[j]==k).其中 + 1 +1 +1是 a [ i ] a[i] a[i]和 a [ j ] a[j] a[j]单独构成等差数列的情况
- 将枚举 i , j , k i,j,k i,j,k优化为仅枚举 i , j i,j i,j:利用 k = a [ i ] − a [ j ] k=a[i]-a[j] k=a[i]−a[j]
- 等差数列的公差可正可负,加一个足够大的数将公差全部变为正数
- f [ i ] [ a [ i ] − a [ j ] + m a x h ] + = f [ j ] [ a [ i ] − a [ j ] + m a x h ] + 1 ( j < i ) f[i][a[i]-a[j]+maxh]+=f[j][a[i]-a[j]+maxh]+1(j<i) f[i][a[i]−a[j]+maxh]+=f[j][a[i]−a[j]+maxh]+1(j<i)
- a n s = n + ∑ f [ i ] [ k ] ans=n+\sum f[i][k] ans=n+∑f[i][k],(n为单独一个数的情况), f [ i ] [ k ] f[i][k] f[i][k]可用上一条公式代换
#include<iostream>
#include<algorithm>
#include<cstring>
typedef long long ll;
using namespace std;
ll ans,mod=998244353,f[1010][40010];
int h[1010],tot,maxh=20010,n;
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>h[i];
}
for(int i=2;i<=n;i++){
for(int j=1;j<i;j++){
f[i][h[i]-h[j]+maxh]=(f[i][h[i]-h[j]+maxh]+f[j][h[i]-h[j]+maxh]+1)%mod;
ans=(ans+f[j][h[i]-h[j]+maxh]+1)%mod;
}
}
ans=(ans+n)%mod;
cout<<ans<<endl;
}
P1077 摆花
#include<iostream>
#include<algorithm>
using namespace std;
int n,m,a[110],f[110][110],mod=1000007;
//f[i][j]用前i种花组成j盆的方案数
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=0;i<=n;i++){
f[i][0]=1;
}
for(int i=1;i<=n;i++){
for(int k=0;k<=a[i];k++){//第i种花选k盆
for(int j=0;j<=m-k;j++){
//从已装的j盆转移过来
if(k==0&&j==0) continue;
f[i][j+k]+=f[i-1][j];
f[i][j+k]%=mod;
}
}
}
cout<<f[n][m]<<endl;
}
P1233 木棍加工
狄尔沃斯(Dilworth)定理:对于任意有限偏序集,其最大反链中元素的数目必等于最小链划分中链的数目;其最长链中元素的数目必等于其最小反链划分中反链的数目
- 按照木棍的长度排序
- 求宽度的最长上升子序列
#include<iostream>
#include<algorithm>
using namespace std;
int n,dp[5010],ans;
struct stick{
int l,w;
bool operator<(const stick& t)const{
return l>t.l;
}
}s[5010];
int main(){
cin>>n;
for(int i=0;i<n;i++){
cin>>s[i].l>>s[i].w;
}
sort(s,s+n);
dp[0]=1;
for(int i=1;i<n;i++){
for(int j=0;j<i;j++){
if(s[j].w<s[i].w){
dp[i]=max(dp[i],dp[j]);
}
}
dp[i]+=1;
}
for(int i=0;i<n;i++){
ans=max(ans,dp[i]);
}
cout<<ans<<endl;
}
P1091 合唱队形
分别正向、反向求最长上升子序列即可。
#include<iostream>
#include<algorithm>
using namespace std;
int n,t[110],f[110],g[110],ans;
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>t[i];
}
//正向最长上升子序列
f[1]=1;
for(int i=2;i<=n;i++){
for(int j=1;j<i;j++){
if(t[i]>t[j]){
f[i]=max(f[i],f[j]);
}
}
f[i]+=1;
}
//逆向最长上升子序列
g[n]=1;
for(int i=n-1;i>=1;i--){
for(int j=n;j>i;j--){
if(t[i]>t[j]){
g[i]=max(g[i],g[j]);
}
}
g[i]+=1;
}
for(int i=1;i<=n;i++){
ans=max(ans,f[i]+g[i]-1);
}
cout<<n-ans<<endl;
}
P5858 「SWTR-03」Golden Sword
80分代码
- d p [ i ] [ j ] dp[i][j] dp[i][j]表示放入 i i i原料后,锅里总共有 j j j个原料时所得到的最大耐久值
- d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ k ] + a [ i ] ∗ j ) , k ∈ [ j − 1 , m i n ( w , k − 1 + s ) ] dp[i][j]=max(dp[i-1][k]+a[i]*j),k\in[j-1,min(w,k-1+s)] dp[i][j]=max(dp[i−1][k]+a[i]∗j),k∈[j−1,min(w,k−1+s)]
- 初始化为-inf, d p [ 0 ] [ 0 ] = 0 dp[0][0]=0 dp[0][0]=0
#include<iostream>
#include<algorithm>
using namespace std;
int n,w,s;
long long dp[5510][5510],inf=1e18,ans=-inf,a[5510];
int main(){
cin>>n>>w>>s;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=0;i<=n;i++){
for(int j=0;j<=w;j++){
dp[i][j]=-inf;
}
}
dp[0][0]=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=w;j++){
for(int k=j-1;k<=min(w,j+s-1);k++){
dp[i][j]=max(dp[i][j],dp[i-1][k]+a[i]*j);
}
}
}
for(int j=1;j<=w;j++){
ans=max(ans,dp[n][j]);
}
cout<<ans<<endl;
}
100分代码
- 因为 a [ i ] ∗ j a[i]*j a[i]∗j是一个常数,将状态转移方程改为 d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ k ] ) + a [ i ] ∗ j , k ∈ [ j − 1 , m i n ( w , s + j − 1 ) ] dp[i][j]=max(dp[i-1][k])+a[i]*j,k\in[j-1,min(w,s+j-1)] dp[i][j]=max(dp[i−1][k])+a[i]∗j,k∈[j−1,min(w,s+j−1)]
- m a x ( d p [ j ] [ k ] ) max(dp[j][k]) max(dp[j][k])利用单调队列优化
- 由于 k k k的取值范围是 [ j − 1 , m i n ( w , s + j − 1 ) ] [j-1,min(w,s+j-1)] [j−1,min(w,s+j−1)],同时存在 < j <j <j和 > j >j >j的部分,将状态方程改为 d p [ i ] [ j ] = m a x ( , d p [ i − 1 ] [ j − 1 ] , d p [ i − 1 ] [ k ] ) + a [ i ] ∗ j , k ∈ [ j , m i n ( w , s + j − 1 ) ] dp[i][j]=max(,dp[i-1][j-1],dp[i-1][k])+a[i]*j,k\in[j,min(w,s+j-1)] dp[i][j]=max(,dp[i−1][j−1],dp[i−1][k])+a[i]∗j,k∈[j,min(w,s+j−1)],这样只需要 j j j从大到小枚举维护单调队列即可
#include<iostream>
#include<algorithm>
#include<deque>
using namespace std;
int n,w,s;
long long dp[5510][5510],inf=1e18,ans=-inf,a[5510];
int main(){
cin>>n>>w>>s;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=0;i<=n;i++){
for(int j=0;j<=w;j++){
dp[i][j]=-inf;
}
}
dp[0][0]=0;
for(int i=1;i<=n;i++){
//单调递减队列
deque<int>q;q.push_back(0);
for(int j=min(w,i);j>=1;j--){
while(q.size()&&q.front()>j+s-1) q.pop_front();
while(q.size()&&dp[i-1][q.back()]<dp[i-1][j])q.pop_back();
q.push_back(j);
dp[i][j]=max(dp[i-1][q.front()],dp[i-1][j-1])+j*a[i];
}
}
for(int j=1;j<=w;j++){
ans=max(ans,dp[n][j]);
}
cout<<ans<<endl;
}