杂题与思维题口胡
Flip Machines
\(tag\):根号分治,dp,期望,贪心
比较神仙,没想到这还能数据分治。
考虑每次交换的卡片上的数,若\(b_i \leq a_i\) 那么这次操作一定是不优的,反之一定能够更优,能使答案变大。
设\(a_i \geq b_i\)构成集合为 \(P\),\(a_i < b_i\)构成集合为\(Q\)。若一个操作\(x\),\(y\)都在\(P\)内显然不可能使期望变大,若都在\(Q\)内一定最优,能使答案变大。
考虑\(x\),\(y\)在\(P\),\(Q\)中都有的是否选取,可以设\(dp_{i,mask}\)表示其中一个集合选取前\(i\)个中的若干元素和另一个中选取方案为\(mask\)的使答案改变的最大值,显然复杂度可以\(O(n2^S)\)。
然而\(|P|+|Q|=n\),因此选择二者最小的一个集合状压即可,总复杂度\(O(m+n2^{\frac{n}{2}})\)。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
template <class T>
void read(T &x){
x=0;char c=getchar();bool f=0;
while(!isdigit(c)) f=c=='-',c=getchar();
while(isdigit(c)) x=x*10+c-'0',c=getchar();
x=f? (-x):x;
}
const int MAXN=45;
const int MAXM=5e5+5;
const int MAXS=1050000;
int n,m;
int a[MAXN],b[MAXN];
int x[MAXM],y[MAXM];
int bel[MAXN];
vector <int> G[MAXN];
bool vis[MAXN];
bool linked[MAXN][MAXN];
double ans;
double dp[MAXN][MAXS];
void add(int u,int v){
if (linked[u][v]) return;
linked[u][v]=1;
G[u].push_back(v);
}
int a0[MAXN],a1[MAXN],id0[MAXN],id1[MAXN],cnt0,cnt1;
int main(){
read(n);read(m);
for (int i=1;i<=n;i++){
read(a[i]);read(b[i]);
if (a[i]>=b[i]) bel[i]=0;
else bel[i]=1;
}
for (int i=1;i<=m;i++){
read(x[i]);read(y[i]);
if (x[i]==y[i]){
if (b[x[i]]>a[x[i]]) swap(b[x[i]],a[x[i]]);
bel[x[i]]=0;
}
}
for (int i=1;i<=m;i++){
int _x=x[i],_y=y[i];
if (_x==_y) continue;
if (bel[_x]&&bel[_y]){
vis[_x]=vis[_y]=1;
}
else if (bel[_x]||bel[_y]){
add(_x,_y);add(_y,_x);
}
}
for (int i=1;i<=n;i++){
if (bel[i]==0){
a0[++cnt0]=i;
id0[i]=cnt0;
}
else{
if (!vis[i]){
a1[++cnt1]=i;
id1[i]=cnt1;
}
}
if (vis[i]) ans+=((double)a[i]+b[i])*1.0/2.0;
else ans+=a[i];
}
for (int i=0;i<=n;i++){
for (int mask=0;mask<MAXS;mask++) dp[i][mask]=-0x3f3f3f3f;
}
dp[0][0]=0;
if (cnt0<=cnt1){
for (int i=1;i<=n;i++){
if (bel[i]&&!vis[i]){
for (int mask=0;mask<(1<<cnt0);mask++){
if (dp[i-1][mask]==-0x3f3f3f3f) continue;
for(const auto &j:G[i]){
int v=id0[j];
if (!v) continue;
dp[i][mask|(1<<(v-1))]=max(dp[i][mask|(1<<(v-1))],dp[i-1][mask]+((double)b[i]-a[i])*1.0/2.0);
}
}
}
for (int mask=0;mask<(1<<cnt0);mask++) dp[i][mask]=max(dp[i][mask],dp[i-1][mask]);
}
double res=0;
for (int mask=0;mask<(1<<cnt0);mask++){
double cur=dp[n][mask];
for (int j=1;j<=cnt0;j++){
if (mask&(1<<(j-1))){
int x=a0[j];
cur+=((double)b[x]-a[x])*1.0/2.0;
}
}
res=max(res,cur);
}
printf("%.10lf\n",ans+res);
}
else{
for (int i=1;i<=n;i++){
if (!bel[i]){
for (int mask=0;mask<(1<<cnt1);mask++){
if (dp[i-1][mask]==-0x3f3f3f3f) continue;
int tmp=mask;
for(const auto &j:G[i]){
int v=id1[j];
if (!v) continue;
tmp|=(1<<(v-1));
}
dp[i][tmp]=max(dp[i][tmp],dp[i-1][mask]+((double)b[i]-a[i])*1.0/2.0);
}
}
for (int mask=0;mask<(1<<cnt1);mask++) dp[i][mask]=max(dp[i][mask],dp[i-1][mask]);
}
double res=0;
for (int mask=0;mask<(1<<cnt1);mask++){
double cur=dp[n][mask];
for (int j=1;j<=cnt1;j++){
if (mask&(1<<(j-1))){
int x=a1[j];
cur+=((double)b[x]-a[x])*1.0/2.0;
}
}
res=max(res,cur);
}
printf("%.10lf\n",ans+res);
}
}
Ina of the Mountain
\(tag\):贪心,前缀和与差分
题意可以转化为给定一个序列\(a\)和初始全空的序列\(b\),求对\(b\)区间加1最少多少次使得两个序列在模\(k\)意义下相等。
首先没有这个取模的限制是一个经典的问题,考虑\(a\)序列的差分\(c\),答案即为\(\sum \limits_{i=1}^{n}[c_i > 0]c_i\),可以理解成两个相邻的上升的数中第二个数可以部分由第一个数所在区间拓展而来,同理若是下降则第二个数可以完全由第一个数区间拓展而来。
发现原题意可以转化为再将\(a\)序列某些元素加上\(k\)的若干倍,即对\(a\)做任意若干次区间加\(k\)操作后\(\sum \limits_{i=1}^{n}[c_i > 0]c_i\)的最小值。\([l, r]\)区间加又可以用差分转化为\(c_l+k,c_{r+1} - k\)。
由于式子与\(c_i\)的正负有关,考虑对于\(c_i\)正负进行分讨。若 \(c_{i} < 0\) ,那么它不仅不会增大最小值,反而可以与后面\(c_{j} > 0\)的进行操作可能得到更优解。因此直接维护一个小根堆,将\(c_i < 0\)的点放入其中,\(对于 c_i > 0\)的点考虑用堆中最小值能否得到更优解,若能则更新答案并将\(c_i - k\)放入堆中。
由于每个元素\(i\)的贡献固定,所以每次取最小值的正确性显然。本质是反悔贪心?
点击查看代码
#include <bits/stdc++.h>
#define ll long long
void solve() {
int n, k;
std::cin >> n >> k;
std::vector <int> a(n + 1), c(n + 1);
for (int i = 1; i <= n; i++) {
std::cin >> a[i];
a[i] %= k;
c[i] = a[i] - a[i - 1];
}
ll ans = 0;
std::priority_queue <int, std::vector<int>, std::greater<int> > q;
for (int i = 1; i <= n; i++) {
if (c[i] < 0) q.push(c[i]);
else {
if (!q.size()) {
ans += c[i];
continue;
}
int x = q.top();q.pop();
if (x + k <= c[i]) {
ans += x + k;
c[i] -= k;
q.push(c[i]);
}
else {
ans += c[i];
q.push(x);
}
}
}
std::cout << ans << "\n";
}
int main() {
std::ios::sync_with_stdio(0);
std::cin.tie(0);
std::cout.tie(0);
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}
Decreasing Game
\(tag:\)博弈,观察,背包
看完题解感觉降智了
发现两人每次操作中,两个数的差不变,更进一步地,包含两个数的不相交的两个集合中元素总和不变。
因此考虑将原序列分成两个,不相交的集合,满足这两个集合内元素的和相等。若可以如此划分,那么最后进过若干次操作后一定是两个集合都为零,后手必胜。反之先手必胜。
集合划分可以用背包记录方案,复杂度\(O(n ^3)\)
做构造,博弈题时一定特别注意不变量,奇偶性等隐含条件
点击查看代码
#include <bits/stdc++.h>
void solve() {
int n;
std::cin >> n;
std::vector <int> a(n + 1);
int sum = 0;
for (int i = 1; i <= n; i++) {
std::cin >> a[i];
sum += a[i];
}
std::vector <std::vector <bool> > dp(n + 1);
std::vector <std::vector <int> > pre(n + 1);
for (int i = 0; i <= n; i++) dp[i].resize(sum + 1), pre[i].resize(sum + 1);
dp[0][0] = 1;
for (int i = 0; i < n; i++) {
for (int j = 0; j <= sum; j++) {
if (dp[i][j]) {
dp[i + 1][j] = 1;
pre[i + 1][j] = j;
if (j + a[i + 1] <= sum) {
dp[i + 1][j + a[i + 1]] = 1;
pre[i + 1][j + a[i + 1]] = j;
}
}
}
}
auto print = [&](auto x) {
std::cout << x << "\n";
std::cout.flush();
};
auto second = [&]() {
std::vector <bool> in(n + 1);
int i = n, j = sum / 2;
do {
if (j != pre[i][j]) in[i] = 1;
j = pre[i][j], i--;
}while (i);
print("Second");
while (1) {
int x;
std::cin >> x;
if (x == 0) return;
for (int i = 1; i <= n; i++) {
if (in[x] != in[i] && a[i]) {
print(i);
int d = std::min(a[x], a[i]);
a[x] -= d, a[i] -= d;
break;
}
}
}
};
auto first = [&]() {
print("First");
while (1) {
int pos;
for (int i = 1; i <= n; i++) {
if (a[i]) {
pos = i;
print(i);
break;
}
}
int x;
std::cin >> x;
if (x == 0) return;
int d = std::min(a[x], a[pos]);
a[x] -= d, a[pos] -= d;
}
};
if (sum % 2 == 0 && dp[n][sum / 2]) second();
else first();
}
int main() {
std::ios::sync_with_stdio(0);
std::cin.tie(0);
std::cout.tie(0);
int t = 1;
while (t--) {
solve();
}
return 0;
}
Present
tag:二进制,拆位
看到异或和,套路地考虑拆位。
枚举答案二进制下第\(k\)位。令 \(b_i = a_i \mod 2^{(k + 1)}(a_i的后k位)\),题意转化为求\(b_i + b_j\)二进制下第\(k\)位是1的对数,发现主要是加法进位不好处理,于是分讨。
统计时需要强制要求和的第\(k\)为1。若\(b_i + b_j\)不会进到第\(k + 1\)位,等价于 \(b_i + b_j \in [2 ^ k,2 ^ {(k + 1)} - 1]\);若\(b_i + b_j\)进第\(k + 1\)位,则等价 \(b_i + b_j \in[2 ^ k + 2 ^ {(k + 1)},2^{(k + 2)} - 1](即[1100...00,1111..11] )\)。
不难想到可以用\(two-pointers\)统计\(b_i + b_j\)小于(或大于)某个值的对数,然后前缀和思想相减即可。
复杂度:\(O(n \log {a_i})\)
点击查看代码
#include <bits/stdc++.h>
void solve() {
int n;
std::cin >> n;
std::vector <int> a(n + 1);
for (int i = 1; i <= n; i++) {
std::cin >> a[i];
}
#define ll long long
auto calc = [&](int x, std::vector<int> &b) {
int p = n;
ll ret = 0;
for (int i = 1; i < p; i++) {
while (i < p && b[i] + b[p] > x) p--;
if (i < p) ret += p - i;
}
return ret;
};//pairs less than or equal to x
auto query = [&](int l, int r, std::vector<int> &b) {
return calc(r, b) - calc(l - 1, b);
};
auto solve = [&](int k) {
std::vector <int> b(n + 1);
for (int i = 1; i <= n; i++) {
b[i] = a[i] % ((1 << (k + 1)));
}
std::sort(b.begin() + 1, b.end());
ll cnt = 0;
cnt += query(1 << k, (1 << (k + 1)) - 1, b);
cnt += query((1 << k) + (1 << (k + 1)), (1 << (k + 2)) - 1, b);
return cnt & 1;
};
int ans = 0;
for (int k = 0; k <= 25; k++) {
if (solve(k)) ans += (1 << k);
}
std::cout << ans << "\n";
}
int main() {
std::ios::sync_with_stdio(0);
std::cin.tie(0);
std::cout.tie(0);
int t = 1;
while (t--) {
solve();
}
return 0;
}