【做题】uoj#370滑稽树上滑稽果——巧妙dp

一个显然的结论是最终树的形态必然是一条链。具体证明只要考虑选定树上的某一条链,然后把其他部分全部接在它后面,这样答案一定不会变劣。

那么,一开始的想法是考虑每一位的最后出现位置,但这并不容易实现。注意到最终序列是单调递减的。我们在统计答案之前,把公共位先统计掉,即始终都是1的位。这样,剩下的位的最终结果都是0。这样,我们就避免了在统计时忽略某些数。那么,我们记dp[i]表示当前的结果为i的最小费用。我们在转移时枚举下一个数字是什么。这里无需担心数字的重复放置,因为它并不能让当前的数字发生变化。那么,我们就有如下第5档部分分。

memset(dp,0x3f,sizeof dp);
for (int i = 1 ; i <= n ; ++ i)
  dp[arr[i]] = arr[i];
for (int i = MAX - 1 ; i >= 1 ; -- i)
  for (int j = 1 ; j <= n ; ++ j)
    if ((i & arr[j]) < i)
      dp[i&arr[j]] = min(dp[i&arr[j]],dp[i] + (i&arr[j]));

注意到这里需要(i&arr[j])<i,即\(C_uarr[j] \bigcap i \neq \emptyset\)。那么,我们把所有\(C_uarr[j]\)的子集存下来,然后转移时枚举子集就做到dp时复杂度与n无关了。
时间复杂度大概是:\(O(a^{log_23})\)
看起来很爆炸,但是非常不满。

#include <bits/stdc++.h>
#define rint register int
using namespace std;
typedef long long ll;
const int N = 200010, MAX = 1 << 18;
int arr[N],n,avail[MAX + 5],com;
ll dp[MAX + 5];
#define rev(x) ((x) ^ (MAX - 1))
int main() {
  com = MAX - 1;
  scanf("%d",&n);
  for (rint i = 1 ; i <= n ; ++ i)
    scanf("%d",&arr[i]), com &= arr[i];
  for (rint i = 1 ; i <= n ; ++ i)
    arr[i] ^= com;
  for (rint i = 1 ; i <= n ; ++ i) {
    if (avail[rev(arr[i])]) continue;
    for (rint j = rev(arr[i]) ; j ; j = (j-1) & rev(arr[i]))
      avail[j] = 1;
  }
  memset(dp,0x3f,sizeof dp);
  for (rint i = 1 ; i <= n ; ++ i) dp[arr[i]] = arr[i];
  for (rint i = MAX-1 ; i >= 1 ; -- i) if (dp[i] < dp[0]) {
    for (rint j = i ; j ; j = (j-1) & i) if (avail[j])
      dp[i^j] = min(dp[i^j],dp[i] + (0ll^i^j));
  }
  printf("%lld\n",1ll * com * n + dp[0]);
  return 0;
}



小结:抓住题目的特殊性质是优化的关键。

转载于:https://www.cnblogs.com/cly-none/p/8628280.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值