Fxx and game
青年理论计算机科学家Fxx给的学生设计了一款数字游戏。 一开始你将会得到一个数X,每次游戏将给定两个参数k,t, 任意时刻你可以对你的数执行下面两个步骤之一: 1.X=X−i(1<=i<=t)。 2.若X为k的倍数,X=X/k。 现在Fxx想要你告诉他最少的运行步骤,使X变成1。
第一行一个整数T(1≤T≤20)表示数据组数。 接下来T行,每行三个数X,k,t(0≤t≤106,1≤X,k≤106) 数据保证有解。
输出共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,做笔记。。
设Fi表示将i变为1的最少步骤,如果k∣i,Fi=min{Fj,Fki},否则Fi=min{Fj},其中i−t≤j≤i−1。
用单调队列维护一下就好了。
时间复杂度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;
}