UVA 1289 Stacking Plates——dp

拿到这道题目第一反应就是它很像汉诺塔问题,只是多了划分这个操作,因此第一步我们尝试把它简化成汉诺塔问题

首先明确一点,最终结果等于划分次数加合并次数。设划分次数为x,那么划分x次必然会形成n+x个堆,把这n+x个堆合并成1个堆需要n+x-1次合并操作,因此最终结果就是2*x+n-1,这样我们只用划分次数x就表示出了最终结果。不过为了使问题更接近汉诺塔问题,我们应该用合并次数来表示最终结果,设合并次数为y,根据上面的推理y=n+x-1,最终结果就是2*y-n+1

然后开始dp,首先把所有盘子从小到大排个序,然后按直径离散化一下,dp【i】【j】表示把前i种盘子放到堆j的最小花费,那么dp【i】【j】一定是由dp【i-1】【k】(1<=k<=n)转移而来的,分析一下会发现转移的时候要讨论j!=k和j=k两种情况。当j!=k时,大方向上要把所有堆中种类为i的盘子合并到堆j,然后把堆k上的(1~i-1)种盘子合并到堆j(注意堆k中可能有种类为i的盘子,这种情况下就不用单独移动(1~i-1)种盘子了);当j=k时,大方向上也是要把所有堆中种类为i的盘子合并到堆j,然后把堆j上的(1~i-1)种盘子合并到堆j(可以理解为先要把(1~i-1)种盘子拿起来,然后把所有种类为i的盘子放过来,最后再把(1~i-1)中盘子合并上),但是如果只有堆j中有种类为i的盘子,那么应该什么也不做,这样才是最优的。具体细节推荐自己实现,或者参考我的代码。

如果你发现按照上面所说的写情况很多,思路很乱,可以试试这个优化,即我们可以在dp的过程中使用贪心思想来简化状态总数。考虑这一点,当你在放第i种盘子时,你要进行决策,即把第i种盘子合并到哪个堆里,而分析后会发现最优情况一定是把第i中盘子放在【有第i种盘子】的那些堆中,比如说堆【3 4】、【5 6】、【3 5】,你在移动第3种盘子(即数字3)的时候一定是把它移动到第一堆或者第三堆,移动到第二堆一定不是最优的,有了这个优化再写代码思路就会非常清晰了

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>
#include <set>
using namespace std;
const int INF = 0x3f3f3f3f;
int kase, n, id[10005], vis[55][2505], dp[2505][55];
//id[i]为盘子直径i离散化后的值, vis[i][j]判断堆i中是否出现过第j种盘子, dp[i][j]表示把前i种盘子排好放到堆j的最小花费
vector<int> plates[55];//输入数据
set<int> val;//用于离散化
vector<int> heap[2505];//heap[i]表示第i种盘子对应的堆
set<int>::iterator it;
void init() {//初始化
    memset(dp, INF, sizeof(dp));
    memset(vis, false, sizeof(vis));
    for (int i = 1; i <= n; i++) plates[i].clear();
    val.clear();
    for (int i = 1; i <= 2500; i++) heap[i].clear();
}
int discrete() {//离散化
    int ran = 0;
    for (it = val.begin(); it != val.end(); it++) id[*it] = ++ran;
    return ran;
}
int main() {
    while (~scanf("%d", &n)) {
        init();
        int k, x;
        for (int i = 1; i <= n; i++) {
            scanf("%d", &k);
            while (k--) {
                scanf("%d", &x);
                plates[i].push_back(x);
                val.insert(x);
            }
        }
        int ran = discrete();
        for (int i = 1; i <= n; i++) {
            for (int j = 0; j < plates[i].size(); j++) {
                x = id[plates[i][j]];
                if (!vis[i][x]) {
                    vis[i][x] = 1;
                    heap[x].push_back(i);//保证第x种盘子对应的堆不重复
                }
            }
        }
        for (int i = 0; i < heap[1].size(); i++) dp[1][heap[1][i]] = heap[1].size()-1;
        for (int i = 2; i <= ran; i++) {
            for (int a = 0; a < heap[i].size(); a++) {
                for (int b = 0; b < heap[i-1].size(); b++) {
                    int j = heap[i][a], k = heap[i-1][b], cnt = heap[i].size();
                    if (j == k) dp[i][j] = min(dp[i][j], dp[i-1][j]+(cnt==1?0:cnt));//cnt-1==0?0:cnt-1+1
                    else dp[i][j] = min(dp[i][j], dp[i-1][k]+cnt-1+!vis[k][i]);
                }
            }
        }
        int ans = INF;
        for (int i = 0; i < heap[ran].size(); i++) ans = min(ans, dp[ran][heap[ran][i]]);
        printf("Case %d: %d\n", ++kase, 2*ans-n+1);
    }
    return 0;
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值