您的位置:首页 > 其它

51nod1693 水群

2016-05-27 00:30 239 查看
基准时间限制:0.4 秒 空间限制:524288 KB 分值: 160 难度:6级算法题

众所周知,水群是一件很浪费时间的事,但是其实在水群这件事中,也可以找到一些有意思的东西。 比如现在,bx2k就在研究怎样水表情的问题。

首先,bx2k在对话框中输入了一个表情

,接下来,他可以进行三种操作。

第一种,是全选复制,把所有表情全选然后复制到剪贴板中。

第二种,是粘贴,把剪贴板中的表情粘贴到对话框中。

第三种,是退格,把对话框中的最后一个表情删去。

假设当前对话框中的表情数是num0,剪贴板中的表情数是num1,那么:

第一种操作就是num1=num0

第二种操作就是num0+=num1

第三种操作就是num0–

现在bx2k想知道,如果要得到n(1<=n<=10^6)个表情,最少需要几次操作。

请你设计一个程序帮助bx2k水群吧。

Input

一个整数n表示需要得到的表情数

Output

一个整数ans表示最少需要的操作数

Input示例

233

Output示例

17

题外话:又是一道神题啊(我实在是太弱了),10^6和0.4秒的组合差点把我吓死。记得BZOJ上也有一道题,操作方式基本上差不多,好像是给定操作数,输出能得到的最多的表情数,高精度+一个奇怪的东西(记不清了)让我直接弃疗了。

让我们先来考虑最暴力的做法,同时记录当前已有数字和剪切板中的数字,直接记忆化搜索,三种转移就不用我再多说了吧。倘若在这道题目上硬要想出什么性质的话恐怕有些困难(也许我太弱了),这就要用到这道题目中最关键的一个思想了——输出中间过程,观察其性质。

不妨让我们来记录一下当前数字是由哪个状态转移过来的,尽管记忆化搜索能跑出来的数据范围比较小,但是再加上我们人类智慧的逻辑推理我们便可以得到过程实际上是这样的(注意x仅代表未知数,不代表具体几次,更不代表其次数相等):

复制*1+粘贴*x(+退格*x)+复制*1+粘贴*x…

其实也很好想吧,连续两次的复制显然是没有意义的,而复制后的退格也可以放到粘贴后面从而对最终得到的结果没有影响,(就这样我用搜索才想到了一个别人可以一眼秒出的结论)既然如此我们为什么不把复制和粘贴看做一个整体呢?于是简化版题意如下:

当前有一个数x,操作1是x∗=k代价为k,操作2是x−−代价为1,求把x从1变到n的最小代价

观察到题目中的操作无非就是由一个数转化到另一个数的时候要付出代价,要求最小化代价。为什么模型好像这么熟悉?经典的最短路模型!连边1:x−>x∗k,连边2:x−>x−1,显然连边数量太庞大了,我们来考虑优化。关键就在于连边1比较恐怖,但我们仔细想想就会发现里边存在大量冗余边。考虑将一个数标准分解k=pa11pa22...,既然我们可以多次复制,为什么我们还需要复制一次后一步步跳到x∗k上去呢(也就是a1∗p1+a2∗p2+...<=k)?由此我们又得到一个优化:连边1转化为只向x∗p连边(p是质数),对结果一定没有影响。

然后我们似乎遇到了瓶颈,好像没有什么优化的方法了,但这时一定要坚定信念,这道题也是出题人出的呀,他又没有用什么量子计算机来使程序跑得更快,所以直觉告诉我们一定还存在优化(哪来的直觉呀!你明明就是看了题解才知道的好吧)!还记得之前说过做这道题需要的重要思想吗?没错,我们再把最短路的转移过程给输出出来!一个神奇的结论在中间过程中显现了出来:①我们只会用到{2,3,5,7,11}这几个数连出去的边(当然事实上我们用到的貌似还可以更少,但是这就已经够了)(前4个质数会在最小120241处出错)(别问我证明,我并不会)。②退格操作不会连续出现4次以上(别问我证明,我并不会)。这样我们就又减少了大量的冗余边,对于0.4秒我们已经可以出解了。我的比较丑陋的代码(懒得删掉调试信息了)(我们的优化还没有结束!如果想知道的话请看代码下方):

#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
const int N=1000010;
//const int M=1000010;
const int p[6]={2,3,5,7,11,13};
int n,pre
,dis
,q[N+10],head,tail;
//int cnt,point
,next[M],to[M],cost[M];
bool vis
;
/*void add(int u,int v,int w)
{
//printf("%d %d\n",u,v);
++cnt;next[cnt]=point[u];
point[u]=cnt;to[cnt]=v;
cost[cnt]=w;
}
void build(int x)
{
for (int i=0;i<6;++i)
{
if(p[i]*x>n+20)break;
add(x,p[i]*x,p[i]);
}
}*/
#define v (u*p[i])
#define vv (u-1)
void spfa()
{
memset(dis,127,sizeof(dis));
dis[1]=0;q[tail=1]=1;head=0;
while(head!=tail)
{
int u=q[++head];vis[u]=0;
if(head==N)head=0;
for (int i=0;i<6;++i)
if(v<n+20&&dis[v]>dis[u]+p[i])
{
dis[v]=dis[u]+p[i];//pre[v]=u;
if(!vis[v])
{
vis[v]=1;q[++tail]=v;
if(tail==N)tail=0;
}
}
if(u>0&&dis[vv]>dis[u]+1)
{
dis[vv]=dis[u]+1;//pre[vv]=u;
if(!vis[vv])
{
vis[vv]=1;q[++tail]=vv;
if(tail==N)tail=0;
}
}
}
}
#undef v
#undef vv
int main()
{
scanf("%d",&n);
//for (int i=1;i<=top;++i)printf("%d ",prime[i]);
//for (int i=1;i<=n;++i)build(i);
//for (int i=1;i<=(n<<1);++i)add(i,i-1,1);
spfa();printf("%d\n",dis
);
//int now=n;while(now!=1){printf("%d ",now);now=pre[now];}
return 0;
}


你想追求比最短路跑得更快的做法?可以!不忘初心,方得始终。让我们再次回到记忆化搜索!f[i][0]表示转移到当前数字i的上一步是由方式1(即连边1)转移而来,而f[i][1]所表示的范围更广,表示用两种方式转移到当前数字i都可以。为什么要这么设计状态呢?因为我们要控制转移方式2连续使用不会连续出现4次以上,所以枚举连续退格几次,然后直接用f[i][0]来强制连续的退格操作终止,不要再加入退格,这样的话DP就不会有环,可以愉快地DP了(额,好像讲得并不明白,看一下代码吧)。由于记忆化搜索是由终点向起点逐步推进,每次都除以一个数,并且记录下当前已得到的结果,所以跑的飞快。

#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#define INF 2147483647/3
using namespace std;
const int N=1000010;
const int p[6]={2,3,5,7,11,13};
int n,f
[2];
inline int ss(int x,bool flag)
//0:only from (x/p[i]);1:both
{
if(x==1)return 0;if(f[x][flag])return f[x][flag];f[x][flag]=INF;
for (int i=0;i<6;++i)//"way No.1"
if(x%p[i]==0)f[x][flag]=min(f[x][flag],ss(x/p[i],1)+p[i]);
if(!flag)return f[x][flag];//else flag==1
f[x][0]=f[x][1];//when f[x][1] isn't updated by "way No.2",f[x][0]=f[x][1]
for (int i=1;i<5;++i)f[x][1]=min(f[x][1],ss(x+i,0)+i);//"way No.2"
return f[x][flag];
}
int main()
{
scanf("%d",&n);
printf("%d\n",ss(n,1));
return 0;
}


总结:

1、不要小看搜索,当人类智慧手推性质、优化算法等遇到瓶颈的时候,注意运用输出中间过程这一方法,往往会得到一些神奇的性质和结论。

2、仔细分析答案的过程,分析其中的性质。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: