dp暑假专题 训练记录

A 回文串的最小划分

 题意:给出长度不超过1000的字符串,把它分割成若干个回文字串,求能分成的最少字串数。

#include <iostream>
#include <cstdio>
#include <string.h>
#include <string>
using namespace std;
const int mod = 1e9 + 7;
const int maxn = 1000 + 5;
const int INF = 0x3f3f3f3f;
typedef long long LL;
int dp[maxn];

//根据回文串的特性,两边根据中心对称。于是我们扫描回文串的每个中心,寻找他的回文串,用DP来记录更新
//dp[n] 表示到n为止可以分割出多少个回文串
//如果[Left , Right]是回文串,
//dp[Right] = min(dp[ Right ] , dp[Left-1] + 1);

int main()
{
    int n;
    cin >> n;
    while ( n-- )
    {
        char s[maxn];
        cin >> s+1;
        int L = strlen(s+1);
        for(int i=1;i <= L;i++) dp[i] = i;

        for(int i=1;i <= L;i++)
        {
            for(int j=i,k=i; j <= L && k > 0 ;j++ ,k--)//这种是aabcc形的回文串
            {
                if(s[j] == s[k] )
                    dp[j] = min(dp[j],dp[k-1] +1);
                else
                    break;
            }
            for(int j=i+1,k=i; j <= L && k > 0 ;j++,k--)//这种是aabb形的回文串
            {
                if(s[j] == s[k] )
                    dp[j] = min(dp[j],dp[k-1] +1);
                else
                    break;
            }
        }
        cout<< dp[L]<<endl;
    }
    return 0;
}
A

B - LIS变形

题意:给定一串数字,求一个子串满足一下要求:子串的长度是L=2*n+1,前n+1个数字是严格的递增序列,后n+1个数字是严格的递减序列,例如123454321就是满足要求的一个子串,

输出满足要求的最长的L

思路:正着走一遍LIS,再倒着走一遍LIS,a1[i] 表示正着走的  到i的最长递增子序列的长度,a2[i] 表示倒着着走的  到i的最长递增子序列的长度

   //本题 二分 + LIS 需要巩固一下

#include <iostream>
#include <cstdio>

using namespace std;
const int mod = 1e9 + 7;
const int maxn = 10000 + 5;
const int INF = 0x3f3f3f3f;
typedef long long LL;
int n,len;
int s1[maxn],s2[maxn];
int a1[maxn],a2[maxn];
int B[maxn];
int b_s(int k)
{
    int ans = 0;
    int l = 0,r = len-1; //这里注意 右区间是 len-1
    while ( l <= r)
    {
        int mid = (l+r)/2;
        if(B[mid] >= k ) ans = mid,r = mid-1; //如果k<= B[mid] 往左边查找
        else l = mid +1;
    }
    return ans ;
}

void Lis(int *s,int *a) //a数组存取到i位 的最长递增子序列个数
{
    B[0] = s[0];
    len = 1;
    for(int i=0;i < n;i++)
    {
        if(s[i] > B[len-1])
        {
            B[len++] = s[i];
        }
        else{
            int pos = b_s (s[i]); //二分找s[i]的插入位置
            B[pos] = s[i];
        }
        a[i] = len;
    }

}
int main()
{
    while (cin >> n)
    {
        for(int i=0;i < n;i++){
            cin >> s1[i];
            s2[n-1-i] = s1[i];//把序列倒着读
        }
        Lis(s1,a1), Lis(s2,a2);  //longest increasing substring 最长递增子序列函数
        int ans = 0;
        for(int i=0;i < n;i++)
        {
            ans = max(ans, min(a1[i],a2[n-1-i])); //ans 是 ans 与 当前位 左右最长递增 和 最长递减的较小者
        }
        cout<< 2*ans -1 <<endl;
    }
    return 0;
}
B

C - 区间dp or LCS的变形

题意:给你一个字符串, 求最长的回文子序列, 若答案不唯一, 则输出字典序最小的

思路1:区间dp, dp[i][j]表示字符串s[i]到s[j]构成回文子序列学删除的最小字符数,    path[i][j] 字符串s[i]到s[j]可构成的最长回文子序列

#include <iostream>
#include <cstdio>
#include <string.h>
#include <string>

using namespace std;
const int mod = 1e9 + 7;
const int maxn = 1000 + 5;
const int INF = 0x3f3f3f3f;
typedef long long LL;
//区间dp dp[i][j] 表示字符串s[i]到s[j]构成回文子序列学删除的最小字符数
//path[i][j] 字符串s[i]到s[j]可构成的最长回文子序列
string path[maxn][maxn];
int dp[maxn][maxn];
char s[maxn];
int main()
{
    while (cin >> s+1)
    {
        int L = strlen(s+1);
        for(int i=L;i >= 1;i--)  //这个查找是从 左下到右上进行的
        {
            dp[i][i] = 0;//i到i不用删除
            path[i][i] = s[i];//i到i的最长回文子序列就是本身
            for(int j=i+1;j <= L;j++)
            {
                if(s[i] == s[j]) //就是 s[i][j]这两个字符相同 可以构成回文串
                {
                    dp[i][j] = dp[i+1][j-1]; //所以这时候 需要删除的就是左下角的
                    path[i][j] = s[i] + path[i+1][j-1] +s[j];//只有这一步是构造回文串的
                }
                else if(dp[i+1][j] > dp[i][j-1])
                {
                    dp[i][j] = dp[i][j-1] + 1;
                    path[i][j] = path[i][j-1];
                }
                else if(dp[i+1][j] < dp[i][j-1])
                {
                    dp[i][j] = dp[i+1][j] + 1;
                    path[i][j] = path[i+1][j];
                }
                else{
                    dp[i][j] = dp[i+1][j] + 1;
                    path[i][j] = min(path[i][j-1], path[i+1][j]);
                }
            }

           /* for(int a = 1;a<=L;a++)
            {
                for(int b= 1;b<=L;b++)
                {
                    cout<< path[a][b]<<" ";
                }
                cout<<endl;
            }
            cout<<"----------" <<endl;  */


        }
        cout << path[1][L] <<endl;
    }
    return 0;
}
C

 思路2:转化为lcs, 用结构体进行保存,一维只长度, 一维指构成的字符串 

我们都知道把一个字符串逆序后和原字符串进最长公共子序列,可以计算出它的最长回文串长度。这题不仅要输出回文串,而且还要求是字典序最小的,所以挺难搞的。

设str1是正序字符串,str2是逆序后的字符串
f[i][j].len 表示str1的前i位,str2的前j位,最长公共子串的长度
f[i][j].str 表示str1的前i位,str2的前j位,最长公共子串的最小字典序的字符串

状态转移和正常的LCS差不多,只不过增加了记录字典序最小的字符串

但是最终的f[i][j].str却并不一定是答案,因为计算出来的最长公共子序列不一定就是回文串

例如:
kfclbckibbibjccbej
jebccjbibbikcblcfk

bcibbibc是他们的LCS,但是却不是回文串

但是它的前len/2个一定是回文串的前半部分

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

using namespace std;
using namespace std;
const int mod = 1e9 + 7;
const int maxn = 1000 + 5;
const int INF = 0x3f3f3f3f;
typedef long long LL;

struct node{
    int len;
    string s;
}dp[maxn][maxn];
char s1[maxn],s2[maxn];

int main ()
{
    while (cin >> s1+1)
    {
        int L = strlen(s1+1);
        strcpy(s2+1,s1+1);
        reverse(s2+1,s2+1+L);

        for(int i=1;i <= L;i++)
        {
            for(int j=1;j <= L;j++)
            {
                if(s1[i] == s2[j]){
                    dp[i][j].len = dp[i-1][j-1].len+1;
                    dp[i][j].s = dp[i-1][j-1].s + s1[i];
                }
                else if(dp[i-1][j].len < dp[i][j-1].len){
                    dp[i][j].len = dp[i][j-1].len;
                    dp[i][j].s = dp[i][j-1].s;
                }
                else if(dp[i-1][j].len > dp[i][j-1].len){
                    dp[i][j].len = dp[i-1][j].len;
                    dp[i][j].s = dp[i-1][j].s;
                }
                else{
                    dp[i][j].len = dp[i-1][j].len;
                    dp[i][j].s = min( dp[i-1][j].s ,dp[i][j-1].s);
                }
            }
        }
        string s = dp[L][L].s;
        int l =dp[L][L].len;
        if( l & 1) //l是奇数,所以字符串长度是偶数
        {
            for(int i=0;i<l/2;i++)
                cout<<s[i];
            for(int i=l/2;i>=0;i--)
                cout<<s[i];
            cout<<endl;
        }
        else{
            for(int i=0;i<l/2;i++)
                cout<<s[i];
            for(int i=l/2-1;i>=0;i--)
                cout<<s[i];
            cout<<endl;
        }

    }
    return 0;
}
C_LCS

D.UVA11795——Mega Mans Missions (还不太会)

题意:

  给出初始武器和消灭每个机器人所能获得的武器,消灭每个机器人需要对应的武器。问消灭所有机器人的方法一共有多少种。(N<=16)

分析:

  可以用一个16位的二进制数表示出当前机器人的存在情况,即一个状态。

  首先预处理出每个状态下所能获得的武器。

  然后依据利用DP的方式求解到达每种状态的方法数。

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

const int maxn = 20;
int attack[maxn];       //表示每个机器人能干掉的
int can_attack[1<<16];  //表示每个状态能干掉的
long long dp[1<<16];    //用来统计每种状态的种数
char s[maxn];
int n;

int main ()
{
    //freopen("C:\Users\ixiao\Desktop\in.txt","r",stdin);
    //freopen("C:\Users\ixiao\Desktop\out.txt","w",stdout);
    int T;
    int cas = 1;
    cin >> T;
    while (T--)
    {
        cin >> n;
        for(int i=0; i <= n;i++)
        {
            attack[i] = 0;//刚开始 二进制位都初始为0
            cin >> s;
            for(int j =0; j < n; j++)
            {
                if(s[j] == '1')
                    attack[i] |= (1 << j);//如果这一位可以击破 就给他赋值为1
            }
        }
        //for(int i=0;i<=n;i++)
            //cout<< attack[i] << " ";
        int total = (1 << n) -1;
        for(int st = 0;st <= total; st++)
        {
            can_attack[st] = attack[0];//记录的刚开始的状态
            for(int i=1; i<=n; i++)
            {
                int j=i-1;
                if( st & (1 << j) ) //如果这个状态可以攻击j
                {
                    can_attack[st] |= attack[i];//再把攻击完j 获得武器加上
                }
            }
        }
        memset(dp,0,sizeof(dp));
        dp[0] = 1;
        for(int st = 1;st <= total; st++)
        {
            for(int i = 0;i < n;i++)
            {
                if(st & (1 << i))//如果st的这种状态能够干掉i,
                {
                    if( can_attack[st ^ (1<<i)] & (1 << i) )//那么要由不能干掉i的状态转移过来,即st ^ (1<<i)
                     {
                         dp[st] += dp[st ^ (1<<i)];
                     }
                }
            }
        }
        cout<<"Case "<<cas++ <<": "<<dp[total] <<endl;
    }
    return 0;
}
D

uva 10564 Paths through the Hourglass(DP)

题意:

  有一个沙漏,如下图所示。你可以从第一行的任意一格开始往下走,向左下方或者右下方走,但不能走出沙漏。你的目标是让沿途所有经过的整数之和恰好为一个整数 s ,求出符合上述条件的路径总数,以及打印一条路径。如果有多条路径,选择起点编号最小的,每一行的格子编号从 0 开始。如果仍然有多个解,移动序列(L代表左移,R代表右移)的字典序应最小。如果不存在可行的路径,输出 0,并且在 0 的下面输出一行空行。

思路:

  待续~

  看着别人的题解实现一遍  就有些感觉了    他这个用了 记忆化数组....问题是代码好恶心 ...等以后变得更强了  自己再写写把

#include <iostream>
#include <cstring>
#include <string>
#include <cstdio>
using namespace std;
typedef long long ll;
int start, N, S, v, mp[50][50], vis[50][50][510];
ll dp[50][50][510];//dp[i][j][k]就表示到第i行第j列之后和为k的方法数
string ans;

void initial ()
{
    for(int i=0; i<50; i++)
    {
        for(int j=0; j<50; j++)
        {
            mp[i][j] = 0;
        }
    }
    ans.clear();
    start = -1;
}
void read()//记录整个地图
{
    int c = N;
    for(int i=0;i < 2*N-1;i++)
    {
        for(int j=0;j < c;j++)
            cin >> mp[i][j];
        if(i < N-1) c--;
        else c++;
    }
}

ll DP(int k, int p, int s, int c, string dir)//从mp[k][p]和为s c表示当前行的个数 dir 表示记录的方位
{
    if(s < 0) return 0;   //这就说明没有这种情况 为0
    if(k >= 2*N - 2) //这就已经到最后一行了
    {
        if(s == 0)
        {
            if(ans.length() == 0) ans = dir; //因为是按照顺序来的 所以第一个被访问到的  肯定就是字典序最小的
            return 1;
        }
        return 0;
    }
    if( vis[k][p][s] == v ) return dp[k][p][s]; //这个用来表示 访问过没有  ==v 是为了更好的判断每个样例
    vis[k][p][s] = v;
    dp[k][p][s] = 0;
    if(k < N-1)//这是从大往小走  然后先往p-1 走然后在走p
    {
        c--;
        if(p-1 >= 0) dp[k][p][s] += DP(k+1,p-1,s-mp[k+1][p-1],c,dir+"L");
        if(p < c) dp[k][p][s] += DP(k+1,p,s-mp[k+1][p],c,dir+"R");
    }
    else//从小往大  先往p 然后p+1
    {
        c++;
        dp[k][p][s] += DP(k+1,p,s-mp[k+1][p],c,dir+"L");
        if(p+1 < c) dp[k][p][s] += DP(k+1,p+1,s-mp[k+1][p+1],c,dir+"R");
    }
    return dp[k][p][s];
}

void solve ()
{
    ll sum = 0;
    if(S <= 351)//351 是因为  (20+19) * 9;
    {
        for(int i=0;i < N;i++){
            sum += DP(0, i, S-mp[0][i], N, "");
            if(S-mp[0][i] >= 0 && dp[0][i][S-mp[0][i]] > 0 &&start == -1 )//这个用来更新start的状态
                start = i;//只更新了最小的  更新完以后start != -1 了  所以没事...
        }
    }
    cout<< sum <<endl;
    if(sum == 0)
        cout<<endl;
    else
        cout<< start <<" " <<ans.c_str() <<endl;

}

int main()
{
    std::ios::sync_with_stdio(false);
    v = 1;
    while (cin>> N >> S && N+S)
    {
        initial();
        read();
        solve();
        v++;
    }
    return 0;
}
E_ DFS实现版本

uva 11552 - Fewest Flops ( 多维dp )

题意: 

  给一个字符串,把它分为k块,例如字符串“helloworld”分成2块,"hello", "world"

  每一块里面的字母可以任意的排序。

  最终字符串, 连续的一样的字母算作一个chunk,问总chunks最少是多少?

思路:

  f[i][j]: 第i块的第j位排在最后一位的最少chunks

   对于每个单独的一块,它的chunks就是等于出现的不同字母数     第i块的chunks记做 chunks[i]    

  如果第i-1块的最后一位和第i块的第一位是一样的,那么可以合并这两个,总chunks可以减少一个    

  f[i][j] = min{  如果i-1块的第k位和i块的第一位不同:  f[i-1][k]+chunks[i], 
                           如果i-1块的第k位和i块的第一位相同:  f[i-1][k]+chunks[i]-1 }

//心静一下
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;

typedef long long ll;
const int INF = 0x3f3f3f3f;
const int maxn = 1010;
int k;
char str[maxn];
int f[maxn][maxn]; // f[i][j]: 第i块的第j位排在最后一位的最少chunks
bool vis [300];

int main ()
{
    std::ios::sync_with_stdio(false);
    int t;
    cin >> t;
    while (t--)
    {
        cin>> k >> str;
        int len = strlen(str);
        memset(f, INF, sizeof(f) );

        for(int i=0; i<len/k; i++)//分块
        {
            int chunks = 0;
            memset(vis , 0, sizeof(vis)) ;
            for (int j = i * k;  j < (i+1)*k; j++)
                vis[ str[j] ] = 1;//标记有这个字符

            for(int j='a'; j <='z';j++)
                if( vis[j] ) ++chunks;//有多少字符 就有多少chunks
            if(i == 0)
            {
                for(int j=0; j<k; j++)
                    f[i][j] = chunks;//把第1个分块 不管怎么排,都是有chunks个
                continue;
            }//no problem

            for(int j=0; j<k; j++)
            {
                int rear = i*k + j;//这个i*k 是 第i个分块的情况
                for(int l=0 ; l<k ; l++)
                {
                    int pre = (i-1)*k+l;//这是i-1分块的情况
                    if(vis[ str[pre] ] && (chunks == 1 || str[pre] != str[rear]))
                    /*
                    vis[ str[pre] ] 说明第i-1个分块 里的pre字符在当前第i个分块里面出现过
                    chunks=1就说明  比如第i-1个分块是 haoran 而第i个分块是nn  所以此时直接-1就可以了
                    而str[pre] != str[rear]  表示rear不与pre相等 就说明当前rear做第i个分块的尾部 而与pre相等的字符做头部
                    这样就可以合并一个了    重点是理解f[i][j] 记录的东西是什么  这道题就好做了
                    */

                        f[i][j] = min(f[i][j], f[i-1][l] + chunks -1);
                    else
                    {
                        f[i][j] = min(f[i][j],f[i-1][l] + chunks);
                    }
                }
            }

        }
        int ans = INF;
        for(int i=0;i<k;i++)
            ans = min(ans,f[len/k -1][i]);
        cout<< ans <<endl;
    }
    return 0;
}
F 充分理解dp方程的含义
原文地址:https://www.cnblogs.com/Draymonder/p/7227809.html