HDU1584 (蜘蛛牌)详解

题目
题目描述
蜘蛛牌是windows xp操作系统自带的一款纸牌游戏,游戏规则是这样的:只能将牌拖到比她大一的牌上面(A最小,K最大),如果拖动的牌上有按顺序排好的牌时,那么这些牌也跟着一起移动,游戏的目的是将所有的牌按同一花色从小到大排好,为了简单起见,我们的游戏只有同一花色的10张牌,从A10,且随机的在一行上展开,编号从110,把第i号上的牌移到第j号牌上,移动距离为abs(i-j),现在你要做的是求出完成游戏的最小移动距离。

输入
第一个输入数据是T,表示数据的组数。
每组数据有一行,10个输入数据,数据的范围是[1,10],分别表示A10,我们保证每组数据都是合法的。

输出
对应每组数据输出最小移动距离。

输入样例
1
1 2 3 4 5 6 7 8 9 10

输出样例
9
 
解析
 
本题有多种解法,这里介绍区间动态规划的解法。
我们用dp[i][j]表示从数字i到数字j的最少合并次数(注意:是“数字”而非“位置”)。
由于小的数字一定可以放在大的数字上,所以相邻的区间一定可以合并。
容易发现,合并后的数字的位置是区间最大的数字的位置,如图:
操作
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
1
-
3
2
4
5
6
7
8
9
10
1
……
--
--
--
--
--
--
--
--
--
--
 
 
 
 
 
 
 
 
10
9
8
7
6
5
4
3
2
1
 
 
 
 
 
 
 
 
10
9
8
7
6
5
4
3
2
1
 
 因此,dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+abs(p[k]-p[j])(这里,p数组存放每个数字的位置)。
 
参考程序
#include <iostream>
#include <string.h>
using namespace std;
int data[10]; //即题目中的p数组
int dp[10][10];
 
inline int abs(int a)
{
    if(a<0) a=-a;
    return a;
}
 
int main()
{
    int t;
    cin>>t;
    while(t--)
    {
        for(int i=0;i<10;i++)
        {
            //处理输入的元素,为了节约空间,这里将位置减一
            int tmp;
            cin>>tmp;
            data[tmp-1]=i;
        }
 //初始化
        memset(dp,0x3F,sizeof(dp));
        for(int i=0;i<10;i++)
        {
            dp[i][i]=0;
        }
 //动态规划过程
        for(int cnt=1;cnt<10;cnt++)
        {
            for(int i=0;i<10-cnt;i++)
            {
                int j=i+cnt;
                for(int k=i;k<j;k++)
                {
                    //状态转移方程
                    dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+abs(data[k]-data[j])); 
                }
            }
        }
        cout<<dp[0][9]<<endl;
    }
    return 0;
}

 

HDU 5691 (Sitting in Line)详解

题目

描述

度度熊是他同时代中最伟大的数学家,一切数字都要听命于他。现在,又到了度度熊和他的数字仆人们玩排排坐游戏的时候了。游戏的规则十分简单,参与游戏的N个整数将会做成一排,他们将通过不断交换自己的位置,最终达到所有相邻两数乘积的和最大的目的,参与游戏的数字有整数也有负数。度度熊为了在他的数字仆人面前展现他的权威,他规定某些数字只能在坐固定的位置上,没有被度度熊限制的数字则可以自由地交换位置。

输入

第一行一个整数T,表示T组数据。
每组测试数据将以如下格式从标准输入读入:

N

a1p1

a2p2



aNPN

第一行,整数 N(1N16),代表参与游戏的整数的个数。

从第二行到第 (N+1) 行,每行两个整数,ai(10000ai10000)pi(pi=1 或 0pi<N),以空格分割。ai代表参与游戏的数字的值,pi代表度度熊为该数字指定的位置,如果pi=1,代表该数字的位置不被限制。度度熊保证不会为两个数字指定相同的位置。

输出

第一行输出:"Case #i:"。i代表第i组测试数据。

第二行输出数字重新排列后最大的所有相邻两数乘积的和,即max{a1a2+a2a3+......+aN1aN}

输入样例

2 6 -1 0 2 1 -3 2 4 3 -5 4 6 5 5 40 -1 50 -1 30 -1 20 -1 10 -1

输出样例

Case #1: -70 Case #2: 4600


解析

本题主要考查状态压缩以及动态规划。

首先考虑采用dp[i]来表示选择某些数字时获得的最大乘积和,为了表示第n个数字被选择,我们将整数i的第n位置为1;反之,第n个数字被选择,我们将整数i的第n位置为0。这样开始的状态为dp[0]而目标状态为dp[2n-1]
状态转移的过程大致如下:
对于每一个状态i遍历i的后继状态h(将i中的某一个0变成1,不妨记这一位为k),然后令dp[h]=min(dp[h],dp[i]+(dp[i]的末尾数字*a[k])。于是问题来了,如何获得dp[i]的末尾数字?

考虑区间动态规划,我们可以考虑先枚举状态i1的个数,再枚举i1的个数为j时的所有状态,对于每一个i的值再枚举1的个数比它少的所以状态。但仅从区间动态规划的实现上看,这样做的时间复杂程度为Ω(23n),此外将两个字状态合并的操作也十分复杂,甚至枚举状态i都需要用到搜素,实际的复杂度高达Θ(24n),当n=16时,大约要执行18446744073709551616次关键语句,远超出时间限制。因此区间动态规划也是不可取的。

其实,为了完成状态转移,我们只需知道上一个状态最后一位的数字就足够了,因此,每个子状态可表示为dp[i][j],其中,i表示所选取的数字的位置(这里的“位置”并非指题目中的p,而是指数字输入时的顺序),j表示末尾数字是i中的第几个1。于是,对于每一个i,我们可以枚举末位位置j(j<i1的计数),再枚举可以添加进状态i中的数k(也就是i中的哪些位是0,这个k将被作为新的末位数字),根据ik计算出i的后继状态h,这样dp[h][k]=max(dp[h][k],dp[i][j]+a[j]*a[k])。时间复杂度为Θ(n2·2n),当n=16时,大约要执行16777216次关键语句,完全可以接受。但有一下细节需要注意:
1、初始化,我们不能将dp数组简单地初始化为0,因为最大的和可能为负数,0x80808080是一个比较好的选择。而对于只有一位是1i,我们应将相应的dp数组中的元素初始化为0.这样我们可以使用memset(dp,0x80,sizeof(dp))来实现快速初始化;
2、当遍历到当前状态i时,i可能不是之前遍历过的任何一个状态的后继状态,这样dp[i][j]0x80808080。这时,继续枚举i的后继状态是没有意义的。我们可以使用continue语句跳过这种情况;
3、注意到,一些数据的位置是固定的,因此,在状态转移前,我们应判断待插入的数字是否固定,如果固定,必须要求待插入的数字在子状态中的位置恰好为固定的位置。随着状态转移,i中的1的个数不断增加,而要插入的数的在新状态中的位置就是i1的个数,我们可以定义一个numof1(int)来计算一个数中i的个数。 


参考程序

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

//用于表示 -∞
#define NINF 0x80808080
using namespace std;

int dp[65536][16];
int num[16];
int place[16];
int len;
short numof1[65536];


//用于获取一个数字中1个数 
short getnum(int a)
{
short cnt=0;
while(a>0)
{
cnt+=a%2;
a>>=1;
}
return cnt;
}


int main()
{
int t,i__=1;
cin>>t;
for(int i=0;i<65536;i++)
{
//优化:将getnum(int)函数的值事先储存在一个数值中
//这样多组测试数据时会大大节约时间 
numof1[i]=getnum(i);
}
while(t--)
{
cin>>len;
int end=(1<<len)-1;
memset(dp,0x80,sizeof(dp));
for(int i=0;i<len;i++)
{
cin>>num[i]>>place[i];
}
//预处理dp数值,将只取一个数字的状态统一设置为0 
for(int i=0;i<len;i++)
{
if(place[i]==-1 || place[i]==0)
{
dp[1<<i][i]=0;
}
}
for(int i=0;i<=end;i++)
{
for(int j=0;j<len;j++)
{
if(dp[i][j]==NINF) continue;
for(int k=0;k<len;k++)
{
if((i&(1<<k))!=0) continue;
if(place[k]!=-1&&place[k]!=numof1[i]) continue;
//h表示下一个状态 
int h=i|(1<<k);
//dp过程 
dp[h][k]=max(dp[h][k],dp[i][j]+num[k]*num[j]);
}
}
}
int ans=NINF;
//遍历所有位都是1的状态,看看以哪个数结尾的状态最大
for(int i=0;i<len;i++)
{
ans=max(ans,dp[end][i]);
}
cout<<"Case #"<<i__++<<":"<<endl<<ans<<endl;
}
return 0;
}