砍竹子(详解)

问题描述

这天, 小明在砍竹子, 他面前有 nn 棵竹子排成一排, 一开始第 ii 棵竹子的 高度为 h_{i}hi​.

他觉得一棵一棵砍太慢了, 决定使用魔法来砍竹子。魔法可以对连续的一 段相同高度的竹子使用, 假设这一段竹子的高度为 HH, 那么

用一次魔法可以 把这一段竹子的高度都变为 \left\lfloor\sqrt{\left\lfloor\frac{H}{2}\right\rfloor+1}\right\rfloor⌊⌊2H​⌋+1​⌋, 其中 \lfloor x\rfloor⌊x⌋ 表示对 xx 向下取整。小明想 知道他最少使用多少次魔法可

让所有的竹子的高度都变为 1 。

输入格式

第一行为一个正整数 nn, 表示竹子的棵数。

第二行共 nn 个空格分开的正整数 h_{i}hi​, 表示每棵竹子的高度。

输出格式

一个整数表示答案。

样例输入

6
2 1 4 2 6 7

样例输出

5

样例说明

其中一种方案:

2 1 4 2 6214267\begin{aligned} &\rightarrow 214262 \\ &\rightarrow 214222 \\ &\rightarrow 211222 \\ &\rightarrow 111222 \\ &\rightarrow 111111 \end{aligned}​→214262→214222→211222→111222→111111​共需要 5 步完成

评测用例规模与约定

对于 20 \%20% 的数据, 保证 n \leq 1000, h_{i} \leq 10^{6}n≤1000,hi​≤106 。 对于 100 \%100% 的数据, 保证 n \leq 2 \times 10^{5}, h_{i} \leq 10^{18}n≤2×105,hi​≤1018 。

运行限制

语言最大运行时间最大运行内存
C++2s256M
C2s256M
Java5s256M
Python310s
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main()
{
  int n,m=0;
  ll skr[50];
  ll f[200010][50];
  ll h,res=0;
  cin>>n;
  int i;
  for(i=0;i<n;i++)
  {
    cin>>h;
    int top=0;
    while(h>1)
    {
       
      skr[top++]=h;
    
      //不能写成 top++,为什么?
      //修改一下,用情景理解来写最好
      //使用0次魔法为h,初始为-1//err
      //为了保险起见,下标最好初始化不为负数,防止数组越界,一定不要赋值下标初始为-1
      h=sqrtl(h/2+1);
    }
    // cout<<top<<endl;
     //如果不确定结果是否正确可以自己先打印出数据看看核对是否程序无误
     //不是最终结果,其中的一部分也是可行的
    res+=top;
    m=max(m,top);
    //得出最大的使用魔法次数,用于接下来,比对满足使用了相同次数的魔法的竹子,
    //省去了很多没必要比对的竹子,大大节省时间.

   for(int j=0,k=top-1;k;j++,k--)
   //这里也不能改格式,如果插入的是1,那你这里就变为-1就越界了
   //可以这么写,但是数据不全.因为可能数组会越界
   //减一是因为top++每次用的都是上一次的数据,所以最终程序走出的时候
   //top是会多一位的,为了对应数组下标所以-1。
   {
     f[i][j]=skr[k];//正序插入到数组
   }
    

  }

    for(int j=0;j<m;j++)//使用魔法的次数先固定,先比对不同的竹子
    for(int i=1;i<n;i++)
    {
      // if(f[i][j]==f[i-1][j])err
      if(f[i][j]&&f[i][j]==f[i-1][j])//要保证j中有数据才能比较
      //没有数据的比较会使得程序出错
      res--;
    }

cout<<res<<endl;


  return 0;
}

 

 防越界就在循环里面改下标

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main()
{
  int n,m=0;
  ll skr[50];
  ll f[200010][50];
  ll h,res=0;
  cin>>n;
  int i;
  for(i=0;i<n;i++)
  {
    cin>>h;
    int top=0;
    while(h>1)//h==1不需要变魔法, 直接不参与统计
    {
       
      skr[++top]=h;//记录变魔法后的长度
      
      //下标1对应初始长度是因为我们舍去了h==1的考虑
      //照理应该是skr[topmax]=1,可是我们直接不要了
      //但又要有正确的变魔法次数,所以数组下标统一后移
      //同时也满足skr下标对应了top
 
     //为了保险起见,下标最好初始化不为负数,防止数组越界,一定不要赋值下标初始为-1
        h=sqrtl(h/2+1);
     }
    // cout<<top<<endl;
     //如果不确定结果是否正确可以自己先打印出数据看看核对是否程序无误
     //不是最终结果,其中的一部分也是可行的
    res+=top;
    m=max(m,top);
    //得出最大的使用魔法次数,用于接下来,比对满足使用了相同次数的魔法的竹子,
    //省去了很多没必要比对的竹子,大大节省时间.
 
   for(int j=0,k=top;k;j++,k--)
  //此时最终满足对应下标而且不会越界,直接插入即可
   {
     f[i][j]=skr[k];//正序插入到数组
   }
    
 
  }
 
    for(int j=0;j<m;j++)//使用魔法的次数先固定,先比对不同的竹子
    //不需要等于m,m次数最大时竹子都砍完了,不会再变魔法了
    //所以都是在变魔法之前判断竹子是否相等
    for(int i=1;i<n;i++)
    {
      // if(f[i][j]==f[i-1][j])err
      if(f[i][j]&&f[i][j]==f[i-1][j])//要保证j中有数据才能比较
      //没有数据的比较会使得程序出错
      res--;
    }
 
cout<<res<<endl;
 
 
  return 0;
}

 两种top的写法效果完全相同,都没有办法避免top无法对齐skr下标的现象

既要满足top不为负数,又要对齐下标,首先只能用++top,必须马上使用才能对齐


首先我们知道++top,与top++最终都不影响top的结果,只不过影响了下标.

最重要的其实在于我们直接舍去了砍竹子最后h==1这个结果(因为1的比对是没有意义的)

删去了这个数据,就要整体下标往右移才能对齐top,因此初始下标为1就好了


总结:如果要舍弃数据就要移动下标对齐。否则要自行加减对齐下标

 下标务必不出现负数,无论什么形式,对齐下标就在下标内使用++top最好(而非top++)


后来又想了想,其实没有必要。我们只需要满足top是正确的就行了。具体在skr的哪个位置

我们并不关心,直接无脑对齐下标++top即可,我们只需要知道skr的初末位下标再重新插回

f数组就行了,至于不同的空间,怕越界就开大点就行了。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main()
{
  int n,m=0;
  ll skr[50];
  ll f[200010][50];
  ll h,res=0;
  cin>>n;
  int i;
  for(i=0;i<n;i++)
  {
    cin>>h;
    int top=0;
    while(h>1)//h==1不需要变魔法, 直接不参与统计
    {
       
      skr[++top]=h;
      //下标1对应初始长度是因为我们舍去了h==1的考虑
      //照理应该是skr[topmax]=1,可是我们直接不要了
      //但又要有正确的变魔法次数,所以数组下标统一后移
      //同时也满足skr下标对应了top

     //为了保险起见,下标最好初始化不为负数,防止数组越界,一定不要赋值下标初始为-1
        h=sqrtl(h/2+1);
     }
    // cout<<top<<endl;
     //如果不确定结果是否正确可以自己先打印出数据看看核对是否程序无误
     //不是最终结果,其中的一部分也是可行的
    res+=top;
    m=max(m,top);
    //得出最大的使用魔法次数,用于接下来,比对满足使用了相同次数的魔法的竹子,
    //省去了很多没必要比对的竹子,大大节省时间.

   for(int j=0,k=top;k;j++,k--)
  //此时最终满足对应下标而且不会越界,直接插入即可
   {
     f[i][j]=skr[k];//正序插入到数组
   }
    

  }

    for(int j=0;j<m;j++)//使用魔法的次数先固定,先比对不同的竹子
    //不需要等于m,m次数最大时竹子都砍完了,不会再变魔法了
    //所以都是在变魔法之前判断竹子是否相等
    for(int i=1;i<n;i++)
    {
      // if(f[i][j]==f[i-1][j])err
      if(f[i][j]&&f[i][j]==f[i-1][j])//要保证j中有数据才能比较
      //没有数据的比较会使得程序出错
      res--;
    }

cout<<res<<endl;


  return 0;
}

 解析补充

// 观察知道每次魔法从首位开始寻找,最大的开始变魔法,并且只有相同的才能一起变。

// 首先,易知变魔法的次数一定会比竹子个数少的多,我们就可以加以利用这个数据。

// 我们可以把所有的竹子长度分层,以变魔法的次数分层。
// 先对每一个竹子变魔法,然后统计所有竹子的变魔法的次数。然后每次变一次魔法,
// 我们就找有没有相同竹子长度。然后减去相同的,就相当于减去每个竹子重复变魔法的次数。

// 因为,只要不相同的,那么它就只能单独变魔法,我根本不在乎谁先变魔法,所以开始时就对每个竹子变魔法。

// 总基数大的肯定是单独变魔法的,所以先统计,然后减去变魔法时相同的竹子。

// 核心就在于抓住变魔法的特点,每一次变魔法,只有相同和不同的情况。不同就单独变,相同的就总共算一次。


#include <iostream>//优先遍历最大的
#include <cstring>
#include <algorithm>
#include <cmath>

using namespace std;

typedef long long LL;

const int N = 200010, M = 20;

int n, m;
LL f[N][M];

int main()
{
    scanf("%d", &n);
    LL stk[M];

    int res = 0;
    for (int i = 0; i < n; i++)//不同竹子
    {
        LL x;
        int top = 0;
        scanf("%lld", &x);

        while (x > 1)
            stk[++top] = x, x = sqrtl(x / 2 + 1);

        //sqrtl范围更大的平方根
        res += top;//就单独讨论,单独对一个竹子使用的魔法需要多少次,然后统计总数
        m = max(m, top);//记录最大次数

        for (int j = 0, k = top; k; j++, k--)//j为竹子长度
            f[i][j] = stk[k];//从小到大
    }

    for (int i = 0; i < m; i++)
        for (int j = 1; j < n; j++)//比较不同竹子
            if (f[j][i] && f[j][i] == f[j - 1][i])//相同的减去
                res--;

    printf("%d\n", res);
    //使用魔法次数
    return 0;
}

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值