【dp专题】NOIP真题-DP专题练习

这里学习一下DP的正确姿势。

也为了ZJOI2019去水一下做一些准备

题解就随便写写啦.

后续还是会有专题练习和综合练习的.

P1005 矩阵取数游戏

给出$n \times m$矩阵每次在每一行取n个数,一共取m次,

第i次取数的权值是$2^i$,给出一个取数的顺序,最大化取完所有数的贡献和。

输出贡献和。

对于100%的数据$1\leq n,m \leq 80,0

需要使用高精度,考虑一个DP,$f[i][j][k]$表示第i行,共取j次,其中k次在前面,显然(j-k)在后面

第j次取数在前面取的那么$f[i][j][k]$是由$f[i][j-1][k-1]$转移而来,得dp方程$f[i][j][k]=f[i][j-1][k-1]+2^j \times a[k]$ (取的数在从左往右数第$k$个)

第j次取数在后面取的那么$f[i][j][k]$是由$f[i][j-1][k]$转移而来,得dp方程$f[i][j-1][k]+2^j \times a[m-(j-k)+1]$ (取的数在从左向右数第$m-(j-k)+1$个)

然后整理可得DP方程$f[i][j][k]=max{f[i][j-1][k-1]+2^j \times a[k] , f[i][j-1][k]+2^j \times a[m-(j-k)+1]  }$

考虑边界条件对于$f[i][j][k]$是有意义的当且仅当$0 \leq j \leq k$,然后使用高精度计算(预处理2的幂的Lint数)

复杂度$O(km^3)$,其中k是高精度数的位数可视为常数

code: 

# include 
# define fp(i,s,t) for(int i=s;i<=t;i++)
# define int long long
using namespace std;
const int N=85;
const int L=50;
char s[L];
inline int read()
{
    int X=0,w=0; char c=0;
    while(c<'0'||c>'9') {w|=c=='-';c=getchar();}
    while(c>='0'&&c<='9') X=(X<<3)+(X<<1)+(c^48),c=getchar();
    return w?-X:X;
}
struct Lint{
    int len,num[L];
    Lint () { len=0; memset(num,0,sizeof(num));}
    Lint change(int x) {
        Lint a;
        if (x==0) { a.num[1]=0;a.len=1; return a;}
        a.len=0;
        while (x>0) a.num[++a.len]=x%10,x/=10;
        return a;
    } 
    void print(Lint x) {
        for (int i=x.len;i>=1;i--) putchar(x.num[i]+'0');
        putchar('\n'); 
    }
    Lint operator + (const Lint &t) const {
        Lint ans;
        memset(ans.num,0,sizeof(ans.num));
        ans.len=max(len,t.len);
        for (int i=1;i<=ans.len;i++) {
            ans.num[i]+=num[i]+t.num[i];
            ans.num[i+1]+=ans.num[i]/10;
            ans.num[i]%=10;
        }
        if (ans.num[ans.len+1]>0) ans.len++;
        return ans;
    }
    Lint operator * (const Lint &t) const {
        Lint ans;
        for (int i=1;i<=len;i++)
         for (int j=1;j<=t.len;j++)
          ans.num[i+j-1]+=num[i]*t.num[j];
        for (int i=1;i<=len+t.len;i++) {
            ans.num[i+1]+=ans.num[i]/10;
            ans.num[i]%=10;
        }  
        if (ans.num[len+t.len]>0) ans.len=len+t.len;
        else ans.len=len+t.len-1;
        return ans;
    } 
    bool operator < (const Lint &t) const {
        if (len>t.len) return false;
        else if (lenreturn true;
        for (int i=len;i>=1;i--)
         if (num[i]return true;
         else if (num[i]>t.num[i]) return false;
        return false; 
    }
}rd;
Lint Max(Lint x,Lint y){if (xreturn y; else return x;}
int n,m;
Lint f[N][N],a[N][N],ans;
Lint pw[N];
signed main()
{
    scanf("%lld%lld",&n,&m);
    fp(i,1,n) fp(j,1,m) a[i][j]=rd.change(read());
    pw[0]=rd.change(1); Lint num_2=rd.change(2);
    fp(i,1,m) pw[i]=pw[i-1]*num_2;
    fp(i,1,n) {
        Lint ret=rd.change(0); memset(f,0,sizeof(f));
        fp(j,1,m) fp(k,0,j) {
            if (k!=0) f[j][k]=f[j-1][k-1]+pw[j]*a[i][k];
            if (j-1>=k-1) f[j][k]=max(f[j][k],f[j-1][k]+pw[j]*a[i][m-j+k+1]);
        }
        fp(k,0,m) ret=Max(ret,f[m][k]);
        ans=ans+ret;
    }
    rd.print(ans); 
    return 0;
}
P1005 矩阵取数游戏

P1026 统计单词个数

给出一个长度为m的字符串,和n个字典$S_i$ ,分成k份,每份里面单独处理,若一个位置为单词首字母可以在后匹配出一个字典,

那么有1的贡献,求出一个分配方案使得字符串分成k份后,贡献值最大。

对于100%的数据$1 \leq m,n \leq 200, 1 \leq k \leq 40 , 字典S_i\leq 6$

如果我们知道s的唯一连续划分[l,r]中总贡献是GetVal(l,r),那么就是一个简单的动态规划问题了。

令$f[i][j]$表示前i个字母划分成j份,最大贡献,显然是一个位置k转移过来增加GetVal(k+1,i)这么多贡献

即$f[i][j]=max\{ f[k][j-1]+GetVal(k+1,i)\}$,然后考虑GetVal(l,r)的暴力求解,直接枚举每一位i属于[l,r],然后枚举n个字典暴力匹配

注意判断是否超出r的右边界的限制,不要加进去。

然后如果枚举i的时候在线做GetVal会多个n的复杂度,不妨离线$O(n^3)$预处理出$val[l,r]=GetVal(l,r)$

然后$O(n^3)$的动态规划,显然是跑不满的。

# include 
using namespace std;
string s,th;
char mp[205][205];
int p,k,n,m;
int f[205][205],val[205][205];
bool check(int pos,int limt)
{
    for (int i=1;i<=n;i++) {
        int l=strlen(mp[i]);
        if (pos+l-1>limt||pos+l-1>m) continue;
        bool flag=true;
        for (int j=0;j) 
          if (s[pos+j]!=mp[i][j]) {
              flag=false; break;
          }
        if (flag) return true;  
    }
    return false;
}
int GetVal(int l,int r)
{
    int ret=0;
    for (int i=l;i<=r;i++) if (check(i,r)) ret++;
    return ret; 
}
int main()
{
    scanf("%d%d",&p,&k); s="#";
    for (int i=1;i<=p;i++) cin>>th,s+=th;
    scanf("%d",&n);
    for (int i=1;i<=n;i++) cin>>mp[i];
    m=s.length()-1;
    for (int i=1;i<=m;i++)
     for (int j=i;j<=m;j++)
      val[i][j]=GetVal(i,j);
    for (int i=1;i<=m;i++)
     for (int j=1;j<=i;j++) {
       # define k kk
       for (int k=j-1;k<=i-1;k++)
       f[i][j]=max(f[i][j],f[k][j-1]+val[k+1][i]);
       #undef k
     }
    
    printf("%d\n",f[m][k]);
    return 0;
}
P1026 统计单词个数

P1099 树网的核

bzoj1999 【数据加强版】

给定一棵n个点的带边权无根树,在其直径上求出一段长度不超过s的路径F,使得离路径距离最远的点到路径的距离最短。

求出这个离路径距离最远的点到路径的距离最短的值。

对于100%的数据$1 \leq n \leq 10^5 ,0 \leq w_i \leq 10^3 $

可以证明,路径F存在于任意一条直径上都可以得到答案。

考虑找出一条树的直径,拉成一条链,就是m个点m-1条边的链

二分一个答案MID

然后对于链上每一个点做一个最长链,记为$w_i$然后从链的两端往里跳,如果可以往里跳尽量往里跳(需要使用MID和$w_i$参数)

得到最终的左右区间[Left,Right]显然若区间为空那么一定可以,

若区间不为空,那么扫描[Left,Right]判断其$w_i$是不是超过了$MID$,其(Right-Left)条边权和是不是超过了限制$s$

然后转化为判定类问题。

复杂度$O(max\{N log_2T,n\}) $ 其中T表示最长链长度,N表示最长链节点数,n表示树节点个数。

可以过BZOJ加强版数据

# include 
using namespace std;
const int N=500005;
int dist[N],d[N],head[N],pre[N],w[N],f[N];
int n,m,s,tot,ans;
bool vis[N];
struct rec{
    int pre,to,w;
}a[N<<1];
inline int read()
{
    int X=0,w=0; char c=0;
    while(c<'0'||c>'9') {w|=c=='-';c=getchar();}
    while(c>='0'&&c<='9') X=(X<<3)+(X<<1)+(c^48),c=getchar();
    return w?-X:X;
}
void adde(int u,int v,int w)
{
    a[++tot].pre=head[u];
    a[tot].to=v;
    a[tot].w=w;
    head[u]=tot;
}
void dfs1(int u,int fath,int L)
{
    dist[u]=L;
    for (int i=head[u];i;i=a[i].pre){
        int v=a[i].to;
        if (v==fath) continue;
        pre[v]=u;
        dfs1(v,u,L+a[i].w);
    }
}
void dfs2(int u,int fath,int L)
{
    ans=max(ans,L); vis[u]=true;
    for (int i=head[u];i;i=a[i].pre) {
        int v=a[i].to;
        if (vis[v]) continue;
        dfs2(v,u,L+a[i].w);
    }
}
bool check(int MID)
{
    # define lift Lift
    # define Right Right
    f[0]=0;
    int left=d[0],right=1;
    for (int i=1;i<=d[0];i++) {
        f[i]=max(f[i-1]+dist[i],w[i]);
        if (f[i]>MID) {left=i; break;}
    }
    f[d[0]+1]=0;
    for (int i=d[0];i>=1;i--) {
        f[i]=max(f[i+1]+dist[i-1],w[i]);
        if (f[i]>MID) { right=i; break;}
    }
    if (left>right) return true;
    int Sum=0;
    for (int i=left;i<=right;i++) {
        if (w[i]>MID) return false;
        if (i!=right) Sum+=dist[i];
    }
    if (Sum<=s) return true; 
    else return false;
    #undef left
    #undef right
}
void GetZhiJin()
{
    dfs1(1,0,0);
    int p1=1;
    for (int i=1;i<=n;i++) if (dist[i]>dist[p1]) p1=i;
    dfs1(p1,0,0);
    int p2=1;
    for (int i=1;i<=n;i++) if (dist[i]>dist[p2]) p2=i;
    swap(p1,p2);
    while (p1!=p2) {
        d[++d[0]]=p1;
        for (int i=head[p1];i;i=a[i].pre){
            int v=a[i].to;
            if (v==pre[p1]) dist[d[0]]=a[i].w;
        }
        p1=pre[p1];
    }
    d[++d[0]]=p2;
    dist[d[0]]=0;
}
int main()
{
    n=read();s=read();
    for (int i=2;i<=n;i++) {
        int u=read(),v=read(),w=read();
        adde(u,v,w),adde(v,u,w);
    }
    GetZhiJin();
    int L=0,R=0;
    for (int i=1;i<=d[0];i++) vis[d[i]]=true,R+=dist[i];
    for (int i=1;i<=d[0];i++) {
        ans=0;
        dfs2(d[i],0,0); 
        w[i]=ans;
    } 
    while (L<=R) {
        int mid=(L+R)>>1;
        if (check(mid)) ans=mid,R=mid-1;
        else L=mid+1;
    }
    cout<'\n';
    return 0;
}
BZOJ1999(Luogu P1099) 树网的核

P1351 联合权值

求一棵n个节点的树,所有边权为$1$,距离为$2$的点对权值乘积最大值、权值乘积和,

其中权值乘积和需要对$10007$取模

对于100%的数据$n\leq 2\times 10^5 , 1 \leq w_i \leq 10^3$

枚举每个点考虑这个点连接到的若干个点,设这个集合为S,那么S重的点互相两两配对都是符合条件的点对。

然后考虑最大值显然是S中两个最大的点权相乘,求和显然是每一个点和前面的点贡献和的2倍,维护即可

复杂度O(kn),其中k表示节点的直接儿子数的平均值,可以看做常数

# include
# define int long long
using namespace std;
const int N=200000+10;
const int mo=10007;
int val[N],tot,head[N];
struct rec{int pre,to;}a[N<<1];
void adde(int u,int v)
{
    a[++tot].pre=head[u];
    a[tot].to=v;
    head[u]=tot;
}
signed main()
{
    int n; scanf("%lld",&n);
    int u,v;
    for (int i=2;i<=n;i++) {
        scanf("%lld%lld",&u,&v);
        adde(u,v); adde(v,u);
    }
    for (int i=1;i<=n;i++) scanf("%lld",&val[i]);
    int ans1=0,ans2=0;
    for (int i=1;i<=n;i++) {
        int u=i;
        int ret=0;
        int last_sum=0;
        int max1=0,pos1;
        for (int i=head[u];i;i=a[i].pre){
            int v=a[i].to;
            ret+=last_sum*val[v]%mo; ret%=mo; 
            last_sum+=val[v]%mo; last_sum%=mo; 
            if (max1v;
        }
        int max2=0;
        for (int i=head[u];i;i=a[i].pre) {
            int v=a[i].to;
            if (max2val[v];
        }
        ans1=max(ans1,max1*max2);
        ans2+=(ret<<1)%mo; ans2%=mo;
    }
    cout<' '<'\n'; 
    return 0;
} 
P1351 联合权值

 P1514 引水入城

给出$n \times m$的网格图每个点(i,j)表示该点的高度,然后考虑水只能从高处流向低处。

在第一行设k个取水站然后要求最后一行所有城市都可以流经水,若可能最小化k,若不可能求有多少个城市无法流经水

对于100%的数据$n,m \leq 500$ 

考虑一个$O(n^3)$算法求出第一行每一个点可以覆盖到最后一行多少点,我们可以证明覆盖的一定是一个区间:

若覆盖不是一个区间那么可能存在2条路径相交,而水可以选取任意一条路径流下,那么这两个区间不是独立的,

必然有交集而形成一个独立的1个区间。

然后在给出的若干个区间做线段覆盖若不能覆盖[1,n]计算30%的答案,若能覆盖那么计算剩余70%的答案

贪心做法如下:左端点排序,然后取头线段看后面头在头线段区间之间那么尽可能往右拓展。排序即可。

常数优化,当且仅当第一行高度比两边都高才进行搜索,显然更优(因为ta隔壁到的ta一定可以到)。

// luogu-judger-enable-o2
# include 
using namespace std;
const int N=505;
const int dx[]={-1,0,1,0};
const int dy[]={0,1,0,-1};
int mp[N][N],a[N][N],get[N];
int n,m,num;
bool use[N][N];
struct node{int x,y;};
struct rec{int l,r;}w[N];
queueq;
bool cmp(rec a, rec b){return a.l<b.l;}
inline int read()
{
    int X=0,w=0; char c=0;
    while(c<'0'||c>'9') {w|=c=='-';c=getchar();}
    while(c>='0'&&c<='9') X=(X<<3)+(X<<1)+(c^48),c=getchar();
    return w?-X:X;
}
void bfs(int sx,int sy)
{
    memset(use,false,sizeof(use));
    q.push((node){sx,sy});
    use[sx][sy]=true;
    while (!q.empty())  {
        node u=q.front(); q.pop();
        for (int i=0;i<4;i++) {
            node v; v.x=u.x+dx[i];v.y=u.y+dy[i];
            if (v.x<1||v.x>n||v.y<1||v.y>m||use[v.x][v.y]) continue;
            if (a[u.x][u.y]<=a[v.x][v.y]) continue;
            use[v.x][v.y]=true;
            q.push(v); 
        }
    }
    for (int i=1;i<=m;i++)
     if (use[n][i]) get[i]=true;
    int L=0,R=0;
    for (int i=1;i<=m;i++)
     if (use[n][i]) { L=i; break;}
    for (int i=m;i>=1;i--) 
     if (use[n][i]) { R=i; break;} 
    if (L==0&&R==0) return;
    w[++num]=(rec){L,R};
}
int main()
{
    n=read();m=read();
    for (int i=1;i<=n;i++)
     for (int j=1;j<=m;j++)
      a[i][j]=read();
    for (int i=1;i<=m;i++) bfs(1,i);  
    int ret=0;
    for (int i=1;i<=m;i++) 
         if (get[i]) ret++;
    if (ret<m) {
        printf("0\n%d\n",m-ret);
        return 0;
    }     
    sort(w+1,w+1+num,cmp);
    int i=1,j=1,ans=0;
    while (j<=m) {
        int t=0;
        while (w[i].l<=j) {
            t=max(t,w[i].r);
            i++;
        }
        j=t+1;ans++;
    }
    printf("1\n%d\n",ans);
    return 0;
}
P1514 引水入城

P1850 换教室

有n门课程,分在2n个教室上课c[i]和d[i],然后对教室之间有无向图{v,e}联通,每次从一教室走到另一教室走最短路。

第i门课程默认在c[i]上课,但是可以最多选择m门课程换到d[i]教室上课,有k[i]的期望换教室成功,确定申报若干个教室编号

使得完成n门课程期望行走路程最短,输出期望最短总路程长度。

对于100%的数据,$v \leq 300,n,m \leq 2000,$图可能会有重边和自环

考虑若选择当前教室申报和不选择当前教室申报期望是不同的,或者说一个有期望,一个没有期望。

所以要设计一维状态0/1表示当前教室选不选择申报。

所以显然的设计$f[i][j][0/1]$表示前 i 节课使用 j 次申报其中第 i 节课 不申报/申报 换教室最小期望总路程长度

考虑$f[i][j][0]$的转移,显然是从$i-1$转移而来,

第1种 第$i-1$节课不申请,所以对于$i-1$到$i$是唯一的一种可能期望路径 从$c[i-1]$走到$c[i]$,得$f[i-1][j][0]+dist(c[i-1],c[i])$

第2种 第$i-1$节课申请 , 所以对于$i-1$到$i$有2种可能的期望路径,从$c[i-1]$到$c[i]$($i-1$换课不成功),从$d[i-1]$到$c[i]$($i-1$换课成功),

            得$f[i-1][j][1]+k[i-1]\times dist(d[i-1],c[i]) + (1-k[i-1]) \times dist(c[i-1],c[i])$

    综上所述$f[i][j][0]=max\{ f[i-1][j][1]+k[i-1]\times dist(d[i-1],c[i]) + (1-k[i-1]) \times dist(c[i-1],c[i]) , f[i-1][j][0]+dist(c[i-1],c[i]) \}$

 

考虑$f[i][j][1]$的转移,显然是从$i-1$转移而来,

第1种 第$i-1$节课不申请,所以对于$i-1$到$i$有2种可能的期望路径,从$c[i-1]$到$c[i]$($i$换课不成功),从$c[i-1]$到$d[i]$($i$换课成功),

            得$f[i-1][j-1][0]+k[i]\times dist(c[i-1],d[i])+(1-k[i])\times dist(c[i-1],d[i]) $

第2种 第$i-1$节课申请 , 所以对于$i-1$到$i$有$2 \times 2$种可能的期望路径,

            从$c[i-1]$到$c[i]$($i-1,i$换课不成功),得 $(1-k[i-1]) \times (1-k[i]) \times dist(c[i-1],c[i])$

            从$c[i-1]$到$d[i]$($i-1$换课不成功,$i$换课成功),得$((1-k[i-1])\times k[i] \times dist(c[i-1],d[i]) $

            从$d[i-1]$到$c[i]$($i-1$换课成功,$i$换课不成功),得$k[i-1]\times (1-k[i]) \times dist(d[i-1],c[i])$

            从$d[i-1]$到$d[i]$($i-1,i$换课成功),得$k[i-1] \times k[i] \times dist(d[i-1],d[i]) $

    综上所述 $f[i][j][1]=max \{f[i-1][j-1][1]+(1-k[i-1]) \times (1-k[i])  \times dist(c[i-1],c[i]) +$

         $ ((1-k[i-1])\times k[i] \times dist(c[i-1],d[i]) + $

         $k[i-1]\times (1-k[i]) \times dist(d[i-1],c[i]) + $

         $ k[i-1] \times k[i] \times dist(d[i-1],d[i]) $

         $, f[i-1][j-1][0]+k[i]\times dist(c[i-1],d[i])+(1-k[i])\times dist(c[i-1],d[i]) \}$

然后考虑j=0的边界转移,若 k=0 只能从i-1不换课,转移,而k=1根本不能

边界f[1][0][0]=0,f[1][1][1]=0,f[][][]=INF

答案是$Min\{ f[n][i][0],f[n][i][1] \} (0 \leq i \leq m)$

# include 
# define fp(i,s,t) for (int i=s;i<=t;i++)
# define INF (1<<20)
using namespace std;
const int N=2e3+10;
int c[N],d[N];
double mp[N][N];
int n,m,v,e;
double f[N][N][2],k[N];
double Min(double a,double b)
{
    if (areturn a; else return b;
}
void floyd()
{
    fp(k,1,v) fp(i,1,v) fp(j,1,v)
        if (k!=i&&i!=j&&j!=k)
         mp[i][j]=min(mp[i][j],mp[i][k]+mp[k][j]);     
}
double D(int x,int y){return mp[x][y];}
signed main()
{
    scanf("%d%d%d%d",&n,&m,&v,&e);
    fp(i,1,n) scanf("%d",&c[i]);
    fp(i,1,n) scanf("%d",&d[i]);
    fp(i,1,n) scanf("%lf",&k[i]);
    fp(i,0,v) fp(j,0,v) mp[i][j]=(i==j||i==0||j==0?0:INF);
    fp(i,1,e) {
        int u,v; double w;
        scanf("%d%d%lf",&u,&v,&w);
        mp[u][v]=mp[v][u]=Min(mp[u][v],w);
    }
    floyd();
    fp(i,0,n) fp(j,0,m) f[i][j][0]=f[i][j][1]=INF;
    f[1][0][0]=0.0; f[1][1][1]=0.0;
    fp(i,2,n) {
        f[i][0][0]=f[i-1][0][0]+D(c[i-1],c[i]);
        fp(j,1,min(i,m)) {
            f[i][j][0]=Min(f[i-1][j][0]+D(c[i-1],c[i]),f[i-1][j][1]+k[i-1]*D(d[i-1],c[i])+(1-k[i-1])*D(c[i-1],c[i]));//i没尝试去换
            f[i][j][1]=Min(f[i-1][j-1][0]+k[i]*D(c[i-1],d[i])+(1-k[i])*D(c[i-1],c[i]),
                           f[i-1][j-1][1]+k[i-1]*k[i]*D(d[i-1],d[i])+k[i-1]*(1-k[i])*D(d[i-1],c[i])+(1-k[i-1])*k[i]*D(c[i-1],d[i])+(1-k[i-1])*(1-k[i])*D(c[i-1],c[i]));
        }    
    }
    double ans=INF;
    fp(i,0,m) ans=Min(ans,Min(f[n][i][0],f[n][i][1]));
    printf("%.2lf\n",ans); 
    return 0;
}
P1850 换教室

 P1941 飞扬的小鸟

有n列m行的网格左下角是(0,0),小鸟每一单位时间横坐标右移1单位,纵坐标在时刻i(出发时为0时刻)有两种情况

  • 1. 自由下落,纵坐标下降Y[i];
  • 2. 花费k$(k >0)$的代价上升$k \times$X[i]。

注意一点:当点k次后小鸟高度超过m那么会停留在上边缘。

有k个管道,给出每个管道位置$p_i$,下表面距离x轴高度$L_i$,上表面距离x轴高度$H_i$

管道覆盖处(下表面高度以下,上表面高度以上)、地面(即x轴)不能触碰。

此时会有两种可能,

  • 若从0列任意位置出发不可能到达n列,输出0\n,然后输出最多通过管道数
  • 若从0列任意位置出发能够到达n列,输出1\n,然后输出最小代价(点击次数)

对于70%的数据,$ 5 \leq n\leq 10^3 ,5 \leq m\leq 10^2$

对于85%的数据,$X[i],Y[i]$较为随机,且开启O2常数优化

对于100%的数据,$5 \leq n \leq 10^4$, $5 \leq m \leq 10^3$,$0 \leq k < n$, $0 < X < m$, $0 < Y < m$, $0 < P < n$, $0 \leq L < H \leq m$, $L + 1 < H$

 

01背包和完全背包综合体。

我们会有一个简单的想法,暴力转移,对于以左下角作为(0,0)点的坐标系来说从i-1列到i列可以有两种转移方式。

记f[i][j]表示小鸟到(i,j)位置(即i列j高度)最小代价。

对于一般情况($j\neq m$),显然是从$f[i-1][j+Y[i-1]]$和$f[i-1][j-k\times X[i-1] ]+k$转移而来。

对于特殊情况($j=m$),只能从i-1列m及以下行某一处跳至少1次到达,跳1次跳到顶部$f[i-1][j-k]+1(k \in [j-X[i-1],m])$,跳2次以上跳到顶部$f[i][j-k]+1(k \in [j-X[i-1],m])$,即这次跳到(i,j-X[i-1])最小代价了如果再跳1次一定可以到达顶部。

复杂度O(knm),显然k取决于数据的随机程度,若给出X[i-1]较小那么程序复杂度会较大。

对于本题即使数据已经比较随机,给出X[]和Y[]以上算法在开启O2优化的情况下能够得70-85分

开启O2优化85分代码: 

// luogu-judger-enable-o2
# include 
# define inf (0x3f3f3f3f)
using namespace std;
const int N=1e4+500,M=1e3+500;
int f[N][M],down[N],up[N],x[N],y[N],pou[N];
int n,m,p;
inline int read()
{
    int X=0,w=0; char c=0;
    while(c<'0'||c>'9') {w|=c=='-';c=getchar();}
    while(c>='0'&&c<='9') X=(X<<3)+(X<<1)+(c^48),c=getchar();
    return w?-X:X;
}
int main()
{
    n=read();m=read();p=read();
    for (int i=0;i)
     x[i]=read(),y[i]=read();
    for (int i=0;i<=n;i++) down[i]=0,up[i]=m+1;
    for (int i=1;i<=p;i++) {
        int pos=read();
        down[pos]=read(); up[pos]=read();
        pou[pos]=1;
    }
    memset(f,0x3f,sizeof(f));
    for (int i=0;i<=m;i++) f[0][i]=0;
    int rp;
    for (int i=1;i<=n;i++) {
        for (int j=1;j<=m;j++) {
             if (j==m) {
                for (int k=m-x[i-1];k<=m;k++) {
                       f[i][m]=min(f[i][m],f[i-1][k]+1);
                       f[i][m]=min(f[i][m],f[i][k]+1);
                   }
            }
            int k=1;
             while (true) {
                 if (j-k*x[i-1]<=0||f[i][j]<=k) break;
                 f[i][j]=min(f[i][j],f[i-1][j-k*x[i-1]]+k);
                 k++;
            }
        }
        for (int j=1;j<=m;j++)
            if (j+y[i-1]<=m) f[i][j]=min(f[i][j],f[i-1][j+y[i-1]]);
        for (int j=1;j<=down[i];j++) f[i][j]=inf;
        for (int j=up[i];j<=m;j++) f[i][j]=inf;
        for (int j=1;j<=m;j++) if (f[i][j]!=inf) rp=i;
    }
    int Ans=inf;
    for (int i=1;i<=m;i++) Ans=min(Ans,f[n][i]);
    if (Ans!=inf) {
        printf("1\n%d\n",Ans);
        return 0;
    }
    Ans=0;
    for (int i=1;i<=rp;i++)
     if (pou[i]==1) Ans++;
    printf("0\n%d\n",Ans);
    return 0;
}
P1941 飞扬的小鸟-85pts

考虑优化常数k,主要是做背包的时候枚举了个数k,可以考虑使用上述特判处的思想,从i这一列已经转移成功的那一行转移过来代替枚举的k,充分利用求得的东西,即f[i][j]从f[i][j-X[i-1]]+1转移过来,即跳到(i,j-X[i-1])已经最小化步数,再多点1次就跳到(i,j)了。

复杂度O(nm)

注意上述dp需要注意边界问题,不要出现负数下标,即使排除不可能决策的转移方案。

然后最后赋值水管位置为inf,两个背包分开来转移。

不开启O2优化100分代码: 

# include 
# define inf (0x3f3f3f3f)
using namespace std;
const int N=1e4+50,M=1e3+50;
int f[N][M],down[N],up[N],x[N],y[N],pou[N];
int n,m,p;
inline int read()
{
    int X=0,w=0; char c=0;
    while(c<'0'||c>'9') {w|=c=='-';c=getchar();}
    while(c>='0'&&c<='9') X=(X<<3)+(X<<1)+(c^48),c=getchar();
    return w?-X:X;
}
int main()
{
    n=read();m=read();p=read();
    for (int i=0;i)
     x[i]=read(),y[i]=read();
    for (int i=0;i<=n;i++) down[i]=0,up[i]=m+1;
    for (int i=1;i<=p;i++) {
        int pos=read();
        down[pos]=read(); up[pos]=read();
        pou[pos]=1;
    }
    memset(f,0x3f,sizeof(f));
    for (int i=0;i<=m;i++) f[0][i]=0;
    int rp;
    for (int i=1;i<=n;i++) {
        for (int j=1;j<=m;j++) {
             if (j==m) {
                   for (int k=m-x[i-1];k<=m;k++) {
                    f[i][m]=min(f[i][m],f[i-1][k]+1);
                    f[i][m]=min(f[i][m],f[i][k]+1);
                   }
             }
             if (j-x[i-1]>0) f[i][j]=min(f[i][j],f[i-1][j-x[i-1]]+1);
             if (j-x[i-1]>0) f[i][j]=min(f[i][j],f[i][j-x[i-1]]+1);
        }
        for (int j=1;j<=m;j++)
            if (j+y[i-1]<=m) f[i][j]=min(f[i][j],f[i-1][j+y[i-1]]);
        for (int j=1;j<=down[i];j++) f[i][j]=inf;
        for (int j=up[i];j<=m;j++) f[i][j]=inf;
        for (int j=1;j<=m;j++) if (f[i][j]!=inf) rp=i;
    }
    int Ans=inf;
    for (int i=1;i<=m;i++) Ans=min(Ans,f[n][i]);
    if (Ans<inf) {
        printf("1\n%d\n",Ans);
        return 0;
    }
    Ans=0;
    for (int i=1;i<=rp;i++)
     if (pou[i]==1) Ans++;
    printf("0\n%d\n",Ans);
    return 0;
}
P1941 飞扬的小鸟-100pts

 

转载于:https://www.cnblogs.com/ljc20020730/p/10428131.html

你可能感兴趣的:(【dp专题】NOIP真题-DP专题练习)