您的位置:首页 > 其它

BestCoder Round #89 1002 && HDU 5945 详解(单调队列+DP)加一种错误的方法

2016-11-08 18:23 579 查看


Fxx and game

Accepts: 74

Submissions: 1857

Time Limit: 3000/1500 MS (Java/Others)

Memory Limit: 131072/65536 K (Java/Others)

问题描述
青年理论计算机科学家Fxx给的学生设计了一款数字游戏。

一开始你将会得到一个数\:XX,每次游戏将给定两个参数\:k,tk,t, 任意时刻你可以对你的数执行下面两个步骤之一:

1.\:X = X - i(1 <= i <= t)1.X=X−i(1<=i<=t)。

2.\:2.若\:X\:X为\:k\:k的倍数,X = X / kX=X/k。

现在Fxx想要你告诉他最少的运行步骤,使\:X\:X变成\:11。

输入描述
第一行一个整数\:T(1\leq T\leq20)\:T(1≤T≤20)表示数据组数。

接下来\:T\:T行,每行三个数\:X,k,t(0\leq t\leq10^6,1\leq X,k\leq10^6)X,k,t(0≤t≤10​6​​,1≤X,k≤10​6​​)

数据保证有解。

输出描述
输出共\:T\:T行。

每行一个整数表示答案。

输入样例
2
9 2 1
11 3 3

输出样例
4
3


思路:这题,最开始可能想到的是贪心+BFS,策略就是每次都有两种操作,一个是到减去一个数i,i<t,如果这个数字能被k整除,可以整除。那对整体结果有用的就是x-i,i<t里面可以被k整除的数字跟i-t这个数字,能减掉的最大数字。为什么是这个策略呢,比如你t1 = x-i能被k整除,t2 = x - i不能被k整除,因为是倒着写的,所以他们的操作进行的步数是一样的,都是dp[x]的步数+1,那么t2就是完全没有用的,只是多+了1(除非他直接变成了1),而t1会有用,因为它可以整除k,会变小很多,相同的步数,肯定是前者更有用,然后就是x-t有用了,因为如果非要减一个数字的话,肯定是减去一个最大的数字让他更靠近1。这里为了优化,不能直接枚举t次i,而是直接枚举k的倍数。。。但是这个复杂度是很不稳定的,假设k=1,这样枚举k的倍数其实就相当于枚举i,直接炸掉了,比如:1000000
1 10000,每个队列都枚举1w个数字,因为每个数都是1的倍数啊,只有队列里有10000的时候才能出现1。。

代码:(手写队列,以为这样会快一点)

#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e6+5;
bool book[maxn];
int x, k, t, num;
struct node
{
    int cur, s;
    node() {};
    node(int cc, int ss): cur(cc), s(ss) {}
}q[maxn];

int bfs(void)
{
    int head = 1, tail = 1;
    q[tail++] = node(x, 0);
    while(1)
    {
        node u = q[head++];
        int cur = u.cur, s = u.s;
        book[cur] = 1;  //book数组是用来去重的
        if(cur == 1) return s;
        int st = cur-t;  //最小的数
        if(st <= 1) { st = 1; return s+1; }
        if(!book[st]) q[tail++] = node(st, s+1), book[st] = 1;  //把最小的存进去
        if(cur%k==0 && !book[cur/k]) q[tail++] = node(cur/k, s+1), book[cur/k] =  1;  //如果这个数可以被k整除,加入队列
        int i = st/k; //枚举在x-t 到 x-1之间k的倍数
        if(st%k) i++;  //如果有余数,说明i*k < st,所以要++;
        for(; i <= cur/k; i++)  //不能枚举i这样数据很大,直接枚举k的倍数
        {
            if(i*k != cur && !book[i*k])
            {
                q[tail++] = node(i*k, s+1);
                book[i*k] = 1;
            }
        }
    }
}
int main(void)
{
    int T;
    cin >> T;
    while(T--)
    {
        memset(book, 0, sizeof(book));
        num = 0;
        scanf("%d%d%d", &x, &k, &t);
        printf("%d\n", bfs());
    }
    return 0;
}


接下来就是单调队列写了。。。网上大神说这是个标准单调队列DP,做笔记。。

设\:F_i\:F​i​​表示将\:i\:i变为\:1\:1的最少步骤,如果\:k|i,F_i=min{F_j,F_{\frac{i}{k}}}k∣i,F​i​​=min{F​j​​,F​​k​​i​​​​},否则\:F_i=min{F_j}F​i​​=min{F​j​​},其中\:i-t\leq
j\leq i-1i−t≤j≤i−1。

用单调队列维护一下就好了。

时间复杂度\:O(n)O(n)。

这是BC官方高中生大神给的题解。。。orz。。用一个维护t+1个变化次数的递增单调队列,(因为这个数可以+上t个数,再加上本身一共t+1个)这样每次新进来的都等于队列头+1,因为队列头是他前面t个数变化次数最小的一个。。

理解下这个单调队列dp:这个转移方程就是dp[i] = min{min(dp[i-t],到dp[i-1], dp[i/k])},这个每个数的值本身就是他的下标,用单队记录变化次数,因为维护的范围是t+1个,所以后面的数字可以直接队首+1,因为队首是维护的次数最小的,因为是从前往后统计的,如果这个i|k的话就直接dp[i] = min(dp[i], dp[i/k]+1);

就行。。。。第一次接触,希望以后做这类题可以轻松一点。。

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
using namespace std;
const int maxn = 1e6 + 5;
struct node
{
int pos, val; //pos记录下标,val记录多少次数
}q[maxn];
int dp[maxn];
int main()
{
int x, t, k, T;
scanf("%d", &T);
while(T--)
{
scanf("%d%d%d", &x, &k, &t);
int head = 1, tail = 1;
q[head].pos = 1;  //这里提前给优先队列存进去了一个数
q[head].val = 0;
dp[1] = 0;  //这个是记录每个数的最优解
for(int i = 2; i <= x; i++)
{
while(head <= tail && i - q[head].pos  >= t + 1) head++; //一共t+1个元素
dp[i] = q[head].val + 1; //等于队首+1,因为他在与队首相隔t个范围内,可以直接+到
if(i % k == 0) dp[i] = min(dp[i], dp[i/k]+1);  //加一个特判
while(head < tail && dp[i] < q[tail].val) tail--; //常规的单调队列了
tail++;
q[tail].pos = i;
q[tail].val = dp[i];
}
printf("%d\n", dp[x]);
}
return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: