蓝桥杯---动态规划(1)

1.糖果(状压dp)

方法:状态压缩+简单dp(简称:状压dp)
状压dp方法步骤:

1.预处理好状态
2.使用处理好的状态进行dp

思路:对于每袋糖果,要么选,要么不选。对于当前选择的一袋糖果,如果
这袋糖果里面有之前没有尝过的,那么选择它,如果这袋糖果所有的口味都已经尝过,那么就不要这一袋。那么只需要将当前的尝过的口味的状态表示处理,就可以用简单的01背包去完成这道题。

在这里插入图片描述

#include <iostream>
#include<cstring>
#include<algorithm>
using namespace std;

const int N=(1<<20)+10,INF=0x3f3f3f3f;

int w[N];  //存放每一袋糖果的状态
int dp[N];   //dp[i]=j表示状态为i的情况下最少尝多少袋糖果

int n,m,k;
int main()
{
  cin>>n>>m>>k;   //袋数,口味种类,每袋的数量
  memset(dp,-1,sizeof dp);   
  for(int i=1;i<=n;i++)
  {
    for(int j=0;j<k;j++)
    {
      int c;
      cin>>c;
      w[i]|=(1<<(c-1)); //运算符‘|’的规则是有1则1。
      //解释一下这句话.
      //1<<c表示1往左移动c-1个位置,表示第c位有糖果。
      //然后利用‘|’运算符将这个位上的1添加到w[i]上面。
    }
    dp[w[i]]=1;  //表示w[i]这个状态只需要1袋糖果就可以到达
  }

  dp[0]=0;  //初始化dp,dp的第一步一定要初始化!!!初始化根据实际定义来。这里表示0种口味的状态的最少袋数,就是0
  for(int i=1;i<=n;i++)   //枚举袋数
  {
    for(int j=0;j<(1<<m);j++)   //枚举状态
    {
        if(dp[j]!=-1)  //如果当前状态存在,那么说明这个可以由这个状态去转移。
        {
            if((dp[j|w[i]])==-1) //选择第i袋糖果的状态没有存在,则说明找到了新口味,选它!
              dp[j|w[i]]=dp[j]+1;
            else      //如果选了第i袋的状态和没有选i的状态是一样的。说明(j|w[i])这个状态的最小数之前算过了,那么比较一下选这一袋和之前的袋数哪个小
              dp[j|w[i]]=min(dp[j]+1,dp[j|w[i]]);
        }

        else  //否则这个状态一定是第第一次出现
        {
            continue;
        }
    }
  }

  cout<<dp[(1<<m)-1]<<endl;
  return 0;
}

2.调手表(状压dp)

简单dp+预处理
在这里插入图片描述
在这里插入图片描述

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;

const int N=100100;

int dp[N];


long long int n,k,j;

int main()
{
  cin>>n>>k;

  dp[0]=0;
  for(int i=1;i<n;i++)   //先算出调k不超过当前小时的情况
  {
    dp[i]=i;
    if(i%k==0&&i>=k)dp[i]=i/k;
  }

  for(int i=1;i<n;i++)   //考虑调k步的和之前的比较求出较小值
  {
      j=i*k%n;
      dp[j]=min(dp[j],i);
  }

  for(int i=2;i<n;i++)   //现在对于dp来说,每一个值要么只走k,要么走1和k,但是即走1又走k的情况只存在一个小时内,没有考虑完整
  {
    //要考虑是否走若干1,又调k绕圈到达某分钟步数最小
    dp[i]=min(dp[i],min(dp[(i+n-k)%n],dp[i-1]))+1;
  }

  int res=0;
  for(int i=1;i<n;i++)
    res=max(res,dp[i]);
  cout<<res<<endl;
  return 0;
}

3.矩阵计数(状压dp)

在这里插入图片描述

思路:搜索或者状压dp
状压dp:(1)预处理合法状态。(2)定义dp。 (3)根据dp定义求状态转移方程
这道题的dp定义:dp[i][j][k]表示前i层,第i层的状态是j,第i-1层的状态是j-1的合法方案数

#include <iostream>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;

const int N=1<<6;
vector<int>state; //存放合法状态
int dp[N][N][N];   //dp[i][j][k]表示前i层,第i层状态j,第i-1层状态时k的合理方案数量
int res=0;
int n,m;
bool check(int x)  //挑选出合法状态
{
  //不能有连续3个1
  int sum=0;  //记录1的个数
  while(x)
  {
    if(x&1)  //‘|‘:同1则1
    {
      sum++;
      if(sum>=3)
        return false;
    }
    else
      sum=0;
    x>>=1;
  }
  return true;
}

int main()
{
  cin>>n>>m;
  //预处理状态
  //0表示0,1表示x
  for(int i=0;i<1<<m;i++)
  {
    if(check(i))
      state.push_back(i);
  }

  memset(dp,0,sizeof dp);
  for(int i=0;i<state.size();i++)  //初始化dp
    dp[0][state[i]][0]=1;   //表示第1层状态i时的方案,因为是第一层,所以只要该层的状态合法就行
  
  for(int i=1;i<n;i++)
  {
    for(int j=0;j<state.size();j++)  //第i层
      for(int k=0;k<state.size();k++)   //i-1层
        for(int L=0;L<state.size();L++)  //i-2层
        {
          if((state[j]&state[k]&state[L])==0)  //表示合法状态
            dp[i][state[j]][state[k]]+=dp[i-1][state[k]][state[L]];
        }
  }

  int ans=0;
  for(int i=0;i<state.size();i++)
    for(int j=0;j<state.size();j++)
      ans+=dp[n-1][state[i]][state[j]];
  cout<<ans<<endl;
  return 0;
  
}

用搜索来做

#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;


const int N=1<<6;
int v[N][N];
int n,m;
int ans=0;

bool check()  //检查当前矩阵是否合法
{
    
    for(int i=1;i<=n;i++)//考虑到可能每一行不足三个数
    {
      for(int j=1;j<=m;j++)
      {
        if(j+2<=m)
        {
          if(v[i][j]==1&&v[i][j+1]==1&&v[i][j+2]==1)
            return false;
        }
        if(i+2<=n)
        {
          if(v[i][j]==1&&v[i+1][j]==1&&v[i+2][j]==1)
            return false;
        }
      }
    }
    
  return true; 
}

void dfs(int x,int y)  //一个一个枚举
{
  if(x==n&&y==m+1)
  {
    if(check())ans++;
    return;
  }
  if(y==m+1) //说明该行已经枚举完,下一行
  {
    y=1;  //从新的一行第一列开始
    x++;
  }
  v[x][y]=0;
  dfs(x,y+1);
  v[x][y]=1;
  dfs(x,y+1);
}

int main()
{
  cin>>n>>m;
  //用搜索来做
  //一行一行来枚举
  dfs(1,1);   //从1行1列开始枚举
  cout<<ans<<endl;
  return 0;
  //
}

4.蒙德里安的梦想(状压dp模板题)

5.跳跃(动态规划,搜索)

在这里插入图片描述
在这里插入图片描述
最基本的dp问题。递推即可。对于任意一格,有9种走法。实际上将题目简化一下就是对于任意一步只有两种走法。但是本质是一样的

#include <iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=110;
int w[N][N];
int dp[N][N];   //dp[i][j]表示当前位于(i,j)的位置上的总价值


int n,m;
int main()
{
  cin>>n>>m;
  for(int i=0;i<n;i++)    //记录每一点的权值
    for(int j=0;j<m;j++)
      cin>>w[i][j];
  
  //定义dp,推出dp方程
  //dp[i][j]=max(以(i,j)为等腰三角形顶点,三角形内所有点都是前一步可能的点,注意这个等腰三角形和往前走的三角形是倒着的)

  memset(dp,-0x3f,sizeof dp);
  dp[0][0]=w[0][0];
  for(int i=0;i<n;i++)
    for(int j=0;j<m;j++)
    {
      //每一步都有9种走法
      //往上走3个,往左走3个,斜着走3个
      
      for(int a=1;a<=3;a++)
      {
        if(i>=a)
          dp[i][j]=max(dp[i][j],dp[i-a][j]);
        if(j>=a)
          dp[i][j]=max(dp[i][j],dp[i][j-a]);
      }
      if(i>=1&&j>=1)
        dp[i][j]=max(dp[i][j],dp[i-1][j-1]);
      if(i>=1&&j>=2)
        dp[i][j]=max(dp[i][j],dp[i-1][j-2]);
      if(i>=2&&j>=1)
        dp[i][j]=max(dp[i][j],dp[i-2][j-1]);
      if(!(i==0&&j==0))
        dp[i][j]+=w[i][j];
    }
  cout<<dp[n-1][m-1]<<endl;
  return 0;
}

5.字符排序(逆序对(难))

在这里插入图片描述
在这里插入图片描述

#include<bits/stdc++.h>
using namespace std;
int V , len , now , cnt[27] , sum[27];
int get_max(int len){
    return ((len - (len / 26 + 1)) * (len / 26 + 1) * (len % 26) + (26 - len % 26) * (len / 26) * (len - len / 26)) / 2;
}
bool check(int x , int n){//当前位置i放x字母后,后面的n个位置是否符合要求 
    memset(cnt , 0 , sizeof(cnt));//cnt记录后面n个位置放置的对应字符数量
    int add1 = 0 , add2 = 0;
    for(int j = 26 ; j >= x + 1 ; j --) add1 += sum[j];//1~i-1里边比当前插入字符大的字符数量
    sum[x] ++ ;//当前字符数量增加1
    for(int L = 1 ; L <= n ; L ++){//当前位置i放了x,看当前位置i后面剩下的n个位置能不能放好字母使得达到V的条件,这是一个预测过程 
        //ma保存i位置后面的n个位置能增加的最大逆序对个数 L-1-cnt[j]+num
        //L-1-cnt[j]是当前字符之后的比当前字符小的字符数量的最大值
        //num是1~pos+L-1前的比当前放置字符字典序大的字符数量
        int ma = -1 , pos = 0 , num = 0;
        for(int j = 26 ; j >= 1 ; j --){//枚举i位置后面的n个位置,每个位置放什么字母 
            if(L - 1 - cnt[j] + num > ma){//L - 1是算i+1到i+L的个数(不算i+L自己) 
			// - cnt[j] 如果在i+1到当前位置i+L前有放过字母j,然后当前位置i+L也尝试放j,
			//那当前位置不和前面放j的位置构成逆序对 要减掉前面放j的位置个数 
                ma = L - 1 - cnt[j] + num;
                pos = j;//在L这个位置放上字母j 
            }
            num += sum[j];//num是从1-i位置里大于等于j的数的个数 
			//因为j是从26逆着放到1的 到下一轮循环时,j--假设记为j',那要求的1-i里比j'大的数就是num 
			//那么这个j'会和前面已经确定好的所有属于26~(j'+1)的数构成逆序对
			//其实最后的num是算的所有大于等于1的个数,但是没关系,它不会更新到ma里 
        }
        add2 += ma , cnt[pos] ++;//L这个位置确定下来,放pos这个字母 
    }
    if(now + add1 + add2 >= V) {//now是i这个位置以前的所有逆序对个数,
	//add1是i位置放进x后,x与前面所有字母构成的逆序对 
	//add2是i位置放进x后,后面的位置的所有数能增加的逆序对个数,分为2部分,
	//1、后面位置所有的数与1~i部分的数构成的逆序对个数
	//2、后面所有位置L,分别与从i+1~i+L-1之间的数构成的逆序对个数,然后这些逆序对的和 
        now += add1;//now更新i位置确定好放字母x后,从1~i位置所有逆序对个数 
        return true;
    }
    else {//i这个位置放x不能使逆序对个数>=V 
        sum[x] -- ;
        return false;
    }
}
signed main()
{
    string ans = "";
    cin >> V;
    for(int i = 1 ; ; i ++) {
        if(get_max(i) >= V){//i这个长度的字符串最多能有get_max(i)个逆序对 如果get_max(i) >= V,
		//那么i这个长度就是我们需要的最短长度 
            len = i;
            break ;
        }
    }
    for(int i = 1 ; i <= len ; i ++){//按长度从左往右尝试放字母 
        for(int j = 1 ; j <= 26 ; j ++){//按顺序'a'-'z' 26个字母放   先用1-26代表这26个字母 
            if(check(j , len - i)){//现在是在第i位放j字母  看i后面的len-i个字母能否符合条件 
                ans += char(j + 'a' - 1);//符合条件就把j放在i这个位置 
                break ;//然后尝试放下一个位置 
            }
        }
    }
    cout << ans << '\n';
    return 0;
}

6.子串分值和(哈希表+子串小推导)

在这里插入图片描述

思路:
原始思路是枚举所有的子串,然后对每个子串进行求值,最后求和。但是这样会超时。

换个方法:枚举所有字符,累加另包含当前字符的子串数目。

#include <iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>

using namespace std;

const int N = 1e6 + 10,INF=0x3f3f3f3f;
int visited[1000];
long long int sum = 0;
int main()
{
    string s;
    cin >> s;
    memset(visited, -INF, sizeof visited);
    //我们要算出每个字母的贡献值累加即可
    //(1)当前字母如果在前面出现过,那么以最近一次出现该字母后面的一个字母为起点,字符串最后一格字符为终点。当前字母将
    //起点到终点分为两个部分,两个部分相乘即可

    for (int i = 0; i < s.length(); i++)
    {
        //枚举到当前字符,以当前字符为中心将字符串分为两个部分
        long long int L, R;  //L表示左边的字符个数,R右边.左右相乘
        if (visited[s[i]] <0)  //说明该字符前面没有出现过。
        {
            L = i + 1;
            R = s.length() - i;
        }
        else   //否则出先过,应该从最近一次出现的后一个字符作为起始字符
        {
            L = i - visited[s[i]];
            R = s.length() - i;
        }
        sum += L * R;   //左右相乘
        visited[s[i]] = i;   //更新当前字符的位置
    }
    cout << sum << endl;

    return 0;
}

7.游园安排(最长不上升子序列dp)

在这里插入图片描述
在这里插入图片描述

#include <iostream>
#include<cstring>
#include<algorithm>
using namespace std;

const int N = 1e5 + 10;
int dp[N];  //dp[i]=j表示
string s[N];   //s[1]表示第一个名字
int path[N];  //存放路径
int a[N];   //存放路径

int main()
{
   
    string str;
    cin >> str;
    int k = -1;
    for (int i = 0; i < str.length(); i++)  //预处理字符串 ,先预处理一下名字,将其放入一个string数组
    {
        if (str[i] >= 'A' && str[i] <= 'Z')
        {
            k = k + 1;
            s[k]+=str[i];
        }
        else
            s[k]+=str[i];
    }

    //初始化dp
    for (int i = 0; i <= k; i++)  //k个名字
        dp[i] = 1;
    memset(path, -1, sizeof path);
    memset(a, -1, sizeof a);
    int start = 0;
    int res = 0;
    for (int i = 0; i <= k; i++)
    {
        string temp="";
        for (int j = 0; j < i; j++)
        {
            if (s[i]>s[j])  //要严格满足s[i]>s[j]才行
            {
                if (dp[j] + 1 > dp[i])
                {
                    dp[i] = dp[j] + 1;
                    path[i] = j;
                    temp = s[j];  //将最新的s[j]保存
                }
                else
                    if (dp[j] + 1 == dp[i])  //如果等于,取最小的s[j]
                    {
                      if (temp>s[j])  //如果temp>s[j],说明temp比当前的s[j]大,我们要取小的
                      {
                          path[i] = j;
                          temp = s[j];
                      }
                    }
            }
        }
        if (dp[i] > res)
        {
            res = dp[i];
            start = i;
        }
        else
          if (dp[i] == res)  // 可能有相同长度的子序列,需要挑选出字典序最小的
          {
            if(s[start]>s[i])  //如果之前的最小字典序的子序列大于等于当前的子序列,我们优先选择小的
              start = i;
          } 
    }
       
    int i = 0;
    while (start != -1)
    {
        a[i++] = start;
        start = path[start];
    }
    for (i = res - 1; i >= 0; i--)
        cout<<s[a[i]];
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值