口渴请饮线性DP

  • 目录
    • P1216 数字三角形
    • P1002 过河卒
    • CodeVS 1579 最长严格上升子序列
    • P1809 过河问题
    • P1020 导弹拦截
    • P1091 合唱队形
    • CodeVS 3641 上帝选人
    • P2782 友好城市
    • P1982 小朋友的数字

目录


因为太水了——题记

P1216 数字三角形

九十年代ioi压轴题,零零年代noi压轴题,如今的普及组签到题
多少朝代更迭,古今辛酸
皆付红尘,巷陌笑语中


回归正题
对于该题,我比较喜欢递推做法,因为这样可以明确地将动态规划与贪心分开
递推有两种
第一种是由前推当前
第二种是由当前推后
具体选择视题目而定
比如这道题就要选第一种
一个数更新到下面两个数不能取max,因为这种贪心显然不对
然而若两个数同时更新一个数取max则是最优
等等这不还是贪心嘛

#include
#include
using namespace std;

int const maxn=1101;
int f[maxn][maxn],map[maxn][maxn],ans;

int main()
{
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=i;j++)
            scanf("%d",&map[i][j]);
    f[1][1]=map[1][1];
    //dp题初始化是关键
    for(int i=2;i<=n;i++)
        for(int j=1;j<=i;j++)
            f[i][j]=max(f[i-1][j],f[i-1][j-1])+map[i][j];
    for(int i=1;i<=n;i++)
        ans=max(ans,f[n][i]);
    printf("%d\n",ans);
    return 0;
}

P1002 过河卒

该题思想上没什么难度,但是细节较多
还是上一题的递推思想,由前面的值更新当前的

#include
#include
typedef long long ll;
using namespace std;

ll f[30][30];
int vis[30][30];

int main()
{
    ll m,n,x,y;
    scanf("%lld%lld%lld%lld",&n,&m,&x,&y);
    n+=1,m+=1,x+=1,y+=1;
    //下标搞出负数会越界,我不想基佬紫,于是我就把棋盘平移了一下
    int dx[9]={0,2,2,-2,-2,1,1,-1,-1};
    int dy[9]={0,1,-1,1,-1,2,-2,2,-2};
    //模拟马走日的过程
    for(int i=0;i<=9;i++)
        vis[x+dx[i]][y+dy[i]]=true;
    f[0][1]=1;
    //为什么不初始化起点呢
    //显然是由于先初始化再状态转移会导致初始值被覆盖
    //想要初始化原点也行,转移的时候特判一下就完事了
    for(int i=1;i<=n;++i)
        for(int j=1;j<=m;++j)
            if(!vis[i][j])
                f[i][j]=f[i][j-1]+f[i-1][j];
    printf("%lld\n",f[n][m]);
    return 0;
}

CodeVS 1579 最长严格上升子序列

该题利用记忆化
每当找到一个数子序列的尾部,就扫一遍这个数的前面来更新最优子结构

#include
#include
typedef long long ll;
using namespace std;

ll f[5100],a[5100],ans;

int main()
{
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%lld",&a[i]);
        f[i]=1;
        //初始化,每个数都是一个以该数结尾长度为1的子序列
    }
    for(int i=2;i<=n;i++)
    //i从2开始,养成好习惯
    {
        for(int j=1;j//j无法取到i,好习惯
            if(a[j]1);
        if(f[i]>ans)
            ans=f[i];
    }
    printf("%lld",ans);
    return 0;
}

P1809 过河问题

这个题不能臆想要手模分析
假如河这边只剩一个人,手电肯定在对面
此时最优解是跑得最快的送手电过来,再一起过去
假如河这边有两个人,手电在对面
最优解是跑的最快的送手电过来,这边的两个人一起过去,次快的过来,一起回去
如果是多个人都可以分解成上述两种
因为符合最优子结构,所以上述两种要取更优
设f[i]为i个人过河的最小渡河时间,a[]为排序后的数组
转移如下
f[i]=min(f[i-1]+a[1]+a[i],f[i-2]+a[1]+a[i]+a[2]*2)

#include
#include
#include
using namespace std;

int const maxn=100011;
int a[maxn],f[maxn];

int main()
{
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    sort(a+1,a+1+n);
    f[1]=a[1];
    f[2]=a[2];
    //注意初始化
    for(int i=3;i<=n;i++)
        f[i]=min(f[i-1]+a[1]+a[i],f[i-2]+a[1]+a[i]+a[2]*2);
    printf("%d",f[n]);
    return 0;
}

P1020 导弹拦截

这个题有个证明,就是
Dilworth定理:偏序集的最少反链划分数等于最长链的长度
然后一顿操作就过了

#include
#include
using namespace std;

int const maxn=100011;
int f[maxn],f1[maxn],a[maxn],ans,ans1,n;

int main()
{
    while(scanf("%d",&a[++n])!=EOF)
    {
        f[n]=1;
        f1[n]=1;
    }
    n--;
    for(int i=2;i<=n;i++)
    {
        for(int j=1;jif(a[i]<=a[j])
                f[i]=max(f[i],f[j]+1);
            else
                f1[i]=max(f1[i],f1[j]+1);
        }
        ans=max(ans,f[i]);
        ans1=max(ans1,f1[i]);
    }
    printf("%d\n%d\n",ans,ans1);
    return 0;
}

P1091 合唱队形

这题虽然水,但是挺有意思..
简单来说就是
找以i为尾最长上升子序列
反向找以i为尾的最长上升子序列(即找以i为首的最长下降)
然后枚举中间那个人i就行了

#include
#include
#include
using namespace std;

int f[111][3],a[111],b[111],n,ans;

bool cmp(int x,int y){
    return a[x]>a[y];
}

int main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>a[i];
        b[i]=i;
        f[i][0]=1;
        f[i][1]=1;
    }
    for(int i=1;i<=n;i++)
        for(int j=1;j<=i;j++)
            if(a[i]>a[j])
                f[i][0]=max(f[i][0],f[j][0]+1);
    for(int i=n;i>=1;i--)
        for(int j=n;j>=i;j--)
            if(a[i]>a[j])
                f[i][1]=max(f[i][1],f[j][1]+1);
    for(int i=1;i<=n;i++)
        ans=max(ans,f[i][0]+f[i][1]-1);
    cout<

CodeVS 3641 上帝选人

满足一个条件,排个序
然后找另一个条件的最长子序列

#include
#include
#include
#include

using namespace std;
int const maxn=3000;
int f[maxn],ans,ans2,b[maxn];

struct node
{
    int iq,e;
}a[maxn];

int cmp(int x,int y)
{
    return a[x].iq<=a[y].iq;
    //这里要注意<=和<的区别,详见博客
}

int main()
{
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d%d",&a[i].iq,&a[i].e);
        f[i]=1;
        b[i]=i;
    }
    sort(b+1,b+1+n,cmp);
    //按iq排序
    for(int i=2;i<=n;i++)
    {
        for(int j=1;jif(a[b[i]].e>=a[b[j]].e)
                f[i]=max(f[j]+1,f[i]);
        ans=max(ans,f[i]);
    }
    printf("%d",ans);
    return 0;
}

P2782 友好城市

P1982 小朋友的数字

#include
#include
int const maxn=10000001;
int const inf=-1*0x7f7f7f7f;
long long f[maxn],a[maxn],ans,f2[maxn],t[maxn],s[maxn];

int main()
{
    int n,p,x;
    long long maxx;
    scanf("%d%d",&n,&p);
    maxx=inf;
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&x);
        s[i]=std::max(s[i-1],s[0])+x;
        maxx=std::max(s[i],maxx);
        t[i]=maxx%p;
    }
    ans=f[1]=t[1];
    maxx=inf;
    for(int i=2;i<=n;i++)
    {
        maxx=std::max(maxx,f[i-1]+t[i-1]);
        f[i]=maxx;
        ans=std::max(ans,f[i])%p;
    }
    printf("%lld",ans);
    return 0;
}
//分数不降
//因此i的分数=前一个人的特征值(即i~j的最大字段和)+前一个人的分数 
//一道水题做了2天...说到底还是缺乏对求最大子段和的算法原理的了解

你可能感兴趣的:(题解,noip,DP)