简单的Nim游戏

220 篇文章 2 订阅
23 篇文章 0 订阅

题目

题目描述
给定长度为 n n n 的非负整数序列 { a } \{a\} {a} 。构造长度为 n n n 的序列 { b } \{b\} {b} ,满足 { b } \{b\} {b} 的异或和为 0 0 0 。最小化 ∑ i = 1 n ∣ a i − b i ∣ \sum_{i=1}^{n}|a_i-b_i| i=1naibi

数据范围与约定
n ≤ 15 , a i < 2 30 n\le 15,a_i< 2^{30} n15,ai<230

思路

耿直的暴力

f ( i , s ) f(i,s) f(i,s) 表示前 i i i 个数的异或和为 s s s ,代价最小是多少。枚举第 i + 1 i+1 i+1 个数即可。

时间复杂度 O ( n V 2 ) \mathcal O(nV^2) O(nV2) V V V a a a 的值域)。

状压的 D P \tt{DP} DP

f ( k , S , i , 0 / 1 ) f(k,S,i,0/1) f(k,S,i,0/1) 表示考虑到第 k k k 高的位、前 i i i 个数,当前位的异或值是 0 / 1 0/1 0/1 S S S 是一个 3 3 3 进制状态压缩,存储每一个数字的变化情况。

  • S S S 中为 0 0 0 :没有发生过变动。
  • 1 1 1 :曾经变小过(在更高的某一位,从 1 1 1 变成了 0 0 0 )。
  • 2 2 2 :与 为1 相对。

复杂度 O ( 3 n n log ⁡ V ) \mathcal O(3^nn\log V) O(3nnlogV)

正解的 D P \tt{DP} DP

状态数量太多。问题主要出在 3 n 3^n 3n 这一项。怎样处理这一点?

假设一个数字在某一位变大了,最优情况下,后面的每一位都要成为 0 0 0 ;假设一个数字在某一位变小了,最优情况下,后面的每一位都要成为 1 1 1 。这里的“最优”即代价最小。

然后呢?如果我想对它们进行操作,它们的代价,一定是 2 x 2^x 2x (假设处理到了第 x x x 位)!因为此时后面是满的。
在这里插入图片描述

最优的情况是红色的情况——全 1 1 1 。后面的操作,一定是满打满算的花费 2 x 2^x 2x

既然花费相同,何必开成三进制状态?

f ( k , i , S , x , y ) f(k,i,S,x,y) f(k,i,S,x,y) 表示第 k k k高位、第 i i i 个数字, S S S 是状压,表示 数字是否发生过改变 x x x 代表着在这一位的异或和; y y y 用来 记录更高的位的修改操作带来的异或和改变

这个 y y y 究竟是什么意思呢?还是刚才那个图:我得将后面整体赋值,才能得到“最优情况”。这个值由谁来记录呢?就是 y y y 了。

T h a t    i s    t o    s a y \tt{That\; is\; to\; say} Thatistosay ,对于 S S S 中已经被改变了的值,单独拿出来做一次异或和。然后剩下的工作就是 x x x 的。

于是我们就可以转移了。时间复杂度 O ( 2 n n log ⁡ V ) \mathcal O(2^n n \log V) O(2nnlogV)(有常数 4 4 4 ,因 x , y x,y x,y 的枚举)。

代码

k k k i i i 可以一起滚动,降低空间复杂度。

#include <cstdio>
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
inline int readint(){
	int a = 0; char c = getchar(), f = 1;
	for(; c<'0'||c>'9'; c=getchar())
		if(c == '-') f = -f;
	for(; '0'<=c&&c<='9'; c=getchar())
		a = (a<<3)+(a<<1)+(c^48);
	return a*f;
}
template < class T >
void getMax(T&a,const T&b){if(a<b)a=b;}
template < class T >
void getMin(T&a,const T&b){if(b<a)a=b;}

const int infty = 0x3f3f3f3f;
const int MaxN = 15;

int dp[2][1<<MaxN][2][2];
int a[MaxN], n;

void Solve() {
	n = readint();
	for(int i=0; i<n; ++i)
		a[i] = readint();
	memset(dp,0x3f,sizeof(dp));
	dp[0][0][0][0] = 0;
	int fr = 0, to = 1; // 用来进行滚动数组
	for(int s=30; ~s; --s) { // 当前处理第s位
		for(int i=0; i<n; ++i) { // 考虑第i个数字
			int v = a[i]>>s&1; // a[i]的第s位
			// cost:第一次操作的花费
			int cost = a[i]&((1<<s)-1);
			cost = !v ? (1<<s)-cost : cost+1;
			memset(dp[to],0x3f,sizeof(dp[to]));
			for(int j=0; j<(1<<n); ++j) // 枚举“是否改变过”
			for(int l=0; l<2; ++l) // "x"
			for(int o=0; o<2; ++o){ // "y"
				int x = dp[fr][j][l][o];
				if (j>>i&1) { // 已经被改变过了
					/* 这个数字什么也不做,异或值承载在y上 */
					getMin(dp[to][j][l][o],x);
					/* 将其更改(这一位的异或值会改,后面的不会,在x上修改!) */
					getMin(dp[to][j][l^1][o],x+(1<<s));
					/* 请再看一次图:后面是全1,再次进行修改,后面仍然是全1 */
				} else { /* 没有被修改过 */
					/* 不修改,在x上记录异或值 */
					getMin(dp[to][j][l^v][o], x);
					/* 如果这一位是1,修改意味着后面是全1,将异或值放在y上 */
					/* 注意:这里的x加入了这个异或值,因为y只影响后面 */
					getMin(dp[to][j^(1<<i)][l^v^1][o^v],x+cost);
				}
			}
			swap(fr,to);
		}
		/* 将要到达下一位 */
		/* 在每一位的初始化时,先将y放到x上 */
		/* 这样一来,x=0就是valid的充要条件 */
		for(int j=0; j<(1<<n); ++j){
			/* 将y放到x上 */
			dp[fr][j][1][1] = dp[fr][j][0][1];
			/* 处理掉不合法情况 */
			dp[fr][j][1][0] = infty;
			/* 它是一个不存在的情况。 */
			/* 高位给了你一个1,而你没有东西,你却拿到0? */
			dp[fr][j][0][1] = infty;
			/* dp[0][0] 完全不会变动 */
		}
	}
	int ans = infty;
	for(int i=0; i<(1<<n); ++i){
		getMin(ans,dp[fr][i][1][1]);
		getMin(ans,dp[fr][i][0][0]);
	}
	printf("%d\n", ans);
}

int main() {
	for(int T=readint(); T; --T)
		Solve();
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值