51nod1693 水群

基准时间限制: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>xk ,连边2: x>x1 ,显然连边数量太庞大了,我们来考虑优化。关键就在于连边1比较恐怖,但我们仔细想想就会发现里边存在大量冗余边。考虑将一个数标准分解 k=pa11pa22... ,既然我们可以多次复制,为什么我们还需要复制一次后一步步跳到 xk 上去呢(也就是 a1p1+a2p2+...<=k )?由此我们又得到一个优化:连边1转化为只向 xp 连边( 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[N],dis[N],q[N+10],head,tail;
//int cnt,point[N],next[M],to[M],cost[M];
bool vis[N];
/*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[n]);
    //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[N][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、仔细分析答案的过程,分析其中的性质。

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值