[SMOJ1176]送礼物

来源:互联网 发布:淘宝广告 编辑:程序博客网 时间:2024/04/29 16:11

题目描述

给出一个 nm 列的点阵,“.”表示可通行格子,“#”表示不可通行格子,“K”表示国王的初始位置,“Q”表示王后的位置,“G”表示该格子有一个礼 物。注意:国王、王后、礼物所在的格子可以认为是可通行格子。国王从开始位置出发,国王从当前格子可以走到上、下、左、右四个相邻格子,当然前提是可通行 格子。国王从当前格子走到相邻格子的时间是变化的,这取决于国王手头上收集到的礼物的数量,假如当前国王手头上有 y 个礼物,那么他从当前格子移动到相邻格 子的所用时间是 y+1 秒。一旦国王进入某个有礼物的格子,他可以选择取该格子的礼物,也可以选择不取该格子的礼物。取礼物这个动作可以认为是瞬间完成的, 不需要时间。国王想收集到尽量多的礼物送给王后,但是他到达王后所在的格子不能超过 T 秒,王后不想等太长时间。注意:国王在收集礼物的途中可能多次走到相 同的格子。

输入格式 1776.in

第 1 行:三个整数 nmT1nm501T109
接下来是 nm 列的点阵,‘G’的数量不超过16。只有一个国王,一个王后。

输出格式 1776.out

一个整数,国王在规定时间内,最多可以收集到多少个礼物送给王后?

样例输入 1776.in

5 7 50
#....G#
###G###
#K...Q#
###.###
#G..GG#

样例输出

4


被这题虐得痛并快乐着……编码量应该算是这几天写的最多的吧(反正我写了3k),在本地也调了好半天,不过一交就过了,还是挺开心的。

还是一样,我们考虑一下这题当中哪些因素应该成为状态。如果是基于位置?显然不大可能吧,那样就太大了。秉承我校教练蓝老师一贯的“数据量决定算法”理念,我发现了礼物的数量 G 是很小的。再细想一下,每件礼物是取或不取,这便是典型的状压 DP 特征,可以用它作为状态。

(归纳一下,以后如果遇到题目中某些东西是开或关,取或不取,经过或不经过……等这种“01”类的状态时,就可以往状压 DP 的方向想想,当然还是要考虑一下阶段、子问题和后效性)

题目问的是限定时间内能取的最多礼物数,既然我们都以礼物为状态了,怎么可能还求的是数量呢?那么,要 DP 求的必然是另一个条件——时间了。

不妨设 f[state][j] 为所取礼物集合为 state 且最后取的礼物为 j 所需的最小时间。那么在状态转移的时候,我们就要考虑几个问题:

原题中对于耗时的描述是“假如当前国王手头上有 y 个礼物,那么他从当前格子移动到相邻格子的所用时间是 y+1 秒”。注意,这句话的意思是,取完第 i 件礼物之后,每走一格的耗时才是 i+1 秒。这也就意味着开始时每走一格的时间为 1 秒。而拿礼物是不用时间的,那我们就不去管它。

那么就有

f[S][i]=min{f[S{i}][j]+card(S)×dist(i,j)} if i,jS and dist(i,j)

上面这个转移方程的意思就是,从某一个取了礼物 j 的子问题走到礼物 i,其中 card(S) 为集合 S 的元素个数(因为子问题中已取了 card(S)1 个礼物,从礼物 j 走到礼物 i 每一步所需时间是取的礼物个数加 1,恰好为 card(S)),dist(i,j) 为礼物 ij 间的曼哈顿距离。

这个式子的正确性是显然的。但是我们在做 DP 的时候存在一个效率上的问题。设礼物个数为 G,那么我们的复杂度已经达到了 O(2GG2),把最大值 G=16 代入计算,会发现超过了 1×107,因此常数上要谨慎一些。

对于 card(S) 可以用 O(n) 的方法逐位判断(其实最多也就16位),当然也可以用每一次减去 lowbit 的方法会稍快些,这个其实影响不大。
主要是 dist(i,j) 的部分,显然我们每一次都跑一遍 BFS 会比较浪费。考虑到矩阵的范围不大,其实不妨在 DP 前进行预处理,算出礼物间两两的距离。

还有一个问题,就是国王和皇后。那其实我们不妨将他们也看作两个礼物,统一化处理就可以了。国王的编号定为 0,皇后为 n1
或者还有一种思路(我就是这么写的),不过比较麻烦,就是预处理的时候从国王开始, DP 的时候把国王和皇后去除,最后取答案的时候再枚举每种情况到皇后,这种写法相对来说没有那么鲁棒。

参考代码:

#include <algorithm>#include <cstdio>#include <cstdlib>#include <cstring>#include <iostream>#include <queue>using namespace std;//用pair存坐标,改成习惯的形式#define X first#define Y secondconst int maxn = 50 + 10;const int maxg = 18;const int dirx[] = {-1, 1, 0, 0};const int diry[] = {0, 0, -1, 1};typedef pair <int, int> Point;int n, m, T;char map[maxn][maxn];int dp[1 << maxg][maxg];int cnt = 0;Point gift[maxg]; //礼物的坐标int disPoint[maxn][maxn];//disPoint[i,j]为某点到(i,j)的最短距离,bfs时临时用int disGift[maxg][maxg]; //两礼物间的最短距离,dp时计算用inline bool islegal(int x, int y) { //坐标越界判定    return x >= 0 && x < n && y >= 0 && y < m;}bool visited[maxn][maxn]; //是否访问过void bfs(Point p, int num) { //p为起点,num为起点在gift[]中的下标    queue <Point> q;    while (!q.empty()) q.pop();    memset(visited, false,sizeof visited);    visited[p.X][p.Y] = true;    memset(disPoint, 0, sizeof disPoint);    for (q.push(p); !q.empty(); q.pop()) {        Point cur = q.front();        for (int i = 0; i < 4; i++) {            int newx = cur.X + dirx[i];            int newy = cur.Y + diry[i];            if (islegal(newx, newy) && !visited[newx][newy] && map[newx][newy] != '#') {                disPoint[newx][newy] = disPoint[cur.X][cur.Y] + 1;                q.push(make_pair(newx, newy));                visited[newx][newy] = true;            }        }    }    //算出并保存从当前情况的起始礼物到其他各礼物的最短路    for (int i = 0; i < cnt; i++)        disGift[num][i] = disPoint[gift[i].X][gift[i].Y];}#define lowbit(x) (x & -x)int count(int x) { //求某个数的二进制中1的个数    int ret = 0;    while (x) {        x -= lowbit(x);        ++ret;    }    return ret;}int main(void) {    freopen("1776.in", "r", stdin);    freopen("1776.out", "w", stdout);    scanf("%d%d%d\n", &n, &m, &T);    Point king, queen;    for (int i = 0; i < n; i++) {        gets(map[i]); //puts(map[i]);        for (int j = 0; j < m; j++)            if (map[i][j] == 'G') gift[cnt++] = make_pair(i, j);            else if (map[i][j] == 'K') king = make_pair(i, j);            else if (map[i][j] == 'Q') queen = make_pair(i, j);    }    gift[cnt++] = king;    gift[cnt++] = queen;    //for (int i = 0; i < cnt; i++) printf("%d %d\n", gift[i].X, gift[i].Y);    memset(disGift, 0, sizeof disGift);    for (int i = 0; i + 1 < cnt; i++)        bfs(gift[i], i);    /*    for (int i = 0; i + 1 < cnt; i++) {        for (int j = 0; j + 1 < cnt; j++) printf("%d ", disGift[i][j]);        putchar('\n');    }    */    int kingNum = cnt - 2; //国王(作为礼物)的下标    int upperLim = 1 << kingNum; //DP中枚举状态时不考虑国王和皇后    memset(dp, 0x7f, sizeof dp);    for (int i = 0; i < kingNum; i++) { //初始化,国王到各个礼物的花费        dp[1 << i][i] = disGift[kingNum][i];        //printf("%d %d %d\n", 1 << i, i, dp[1 << i][i]);    }    //puts("prework done");    for (int i = 0; i < upperLim; i++)        for (int j = 0; j < kingNum; j++)            if ((1 << j) & i)                for (int k = 0; k < kingNum; k++)                    if (j != k && ((1 << k) & i) && disGift[j][k]) {                        dp[i][j] = min(dp[i][j], dp[i - (1 << j)][k] + count(i) * disGift[j][k]);                        //printf("%d %d %d %d\n", i, j, k, dp[i][j]);                    }    int queenNum = cnt - 1;    int ans = 0;    for (int i = 0; i < upperLim; i++)        for (int j = 0; j < kingNum; j++) //枚举最后取的礼物            if (disGift[j][queenNum]) { //该礼物要可达皇后才是合法方案                int cost = dp[i][j] + (count(i) + 1) * disGift[j][queenNum];                if (cost <= T) ans = max(ans, count(i));            }    printf("%d\n", ans);    return 0;}
0 0
原创粉丝点击