A. array balance
题意:
有A、B两个数组,都有n个元素,我们可以任意选择交换
a
i
a_{i}
ai 和
b
i
b_{i}
bi,求
m
i
n
(
∑
1
n
−
1
∣
a
i
−
a
i
+
1
∣
+
∣
b
i
−
b
i
+
1
∣
)
min(\sum_{1}^{n-1} |a_{i}-a_{i+1}|+|b_{i}-b_{i+1}|)
min(∑1n−1∣ai−ai+1∣+∣bi−bi+1∣)
思路:
其实一共有n次交换与否的选择,我们贪心做出每次的选择即可
当我想求第i项的最优,我们这里选择是否换通项中的第i+1项,这是有技巧的,因为无论第i项在上一步交换了没有,根本对我求最优没有影响,第i项交换无非也就是从左边的绝对值项跑到右边的绝对值项,右边的跑到左边,都没有区别,因为我现在可以选择是否交换第i+1项,无论你是换还是没换,我都通过换第i+1项找个最优,这其实就是无后效性,这玩意更需要一种直觉来理解。
以此类推,当我想求第i+1项的最优,无论我第i+1项交换了没有,我总能通过交换第i+2项使得当前最优
具体贪心思路:
只要 abs(a[i]-a[i+1])+abs(b[i]-b[i+1])>abs(a[i]-b[i+1])+abs(b[i]-a[i+1])
的话 ,那就交换第i+1个
这里只能是选择第i+1个,不能选择交换第i个,选择交换第i个就有后效性了,我第i+1项的交换与否的选择就会影响我第i项求出的所谓最优
代码:
//其实思路蛮简单,蛮直观,因为它不是要A的sum和B的sum各最大,
//它直接都并成了一个整体:|a1-a2|+|a2-a3|+……+|an-1-an|+|b1-b2|+|b2-b3|+……+|bn-1-bn|
//既然是一个整体,贪心思路很明确,就是贪每个位置交换前后是增加还是减少了,是增加了就可以换
//为啥无后效性呢,第i位贪心决定是否交换,达到局部最优,可是第i+1位继续贪心会影响第i位啊
#include <bits/stdc++.h>
using namespace std;
int a[30];
int b[30];
void Swap(int i)
{
int temp;
temp = a[i];
a[i] = b[i];
b[i] = temp;
}
int main()
{
int t;
cin >> t;
while (t--)
{
int n;
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
}
for (int i = 1; i <= n; i++)
{
cin >> b[i];
}
//遍历位置1-n,看要不要换位置,换之后更大那就换
for (int i = 1; i <= n; i++)
{
if ( abs(a[i] - b[i + 1]) + abs(b[i] - a[i + 1]) < abs(a[i] - a[i + 1]) + abs(b[i] - b[i + 1]) )
{
Swap(i + 1);
}
/*
if ( abs(b[i] - a[i + 1]) + abs(a[i] - b[i + 1]) < abs(a[i] - a[i + 1]) + abs(b[i] - b[i + 1]) )
{
Swap(i);
}
这样是错的
*/
}
long long sum = 0;
for (int i = 1; i < n; i++)
{
sum += abs(a[i] - a[i + 1]) + abs(b[i] - b[i + 1]);
}
cout << sum << endl;
}
}
B. Getting zero
题意:
给定x,给x两个操作:
- x=x+1 mod 32768
- x=2*x mod 32768
问至少需要多少次操作可以将x变为0
思路:明显的bfs求无权图最短路,x有两个操作可以扩展出子状态,搜就完事了
//明显的bfs求最短路,和农夫和牛那题一样
#include <bits/stdc++.h>
using namespace std;
class Node
{
public:
int x, cnt;
Node(int a, int b): x(a), cnt(b) {}
};
int vis[32768 + 5];
int history[32768 + 5];
int main()
{
memset(history, 0, sizeof(history));
int n;
cin >> n;
//既然n个数做的操作都是一模一样的,
//可以做记忆化,求出过的数就直接存起来,下次遇到直接输出即可,
//不必重新再算一遍(就是被重复的3w多个25给hack超时了)
while (n--)
{
int s;
cin >> s;
if (history[s])
{
cout << history[s] << " ";
continue;
}
//bfs
memset(vis, 0, sizeof(vis));
queue<Node>q;
q.push(Node(s, 0));
vis[s] = 1;
while (!q.empty())
{
Node node = q.front();
q.pop();
if (node.x == 0)
{
cout << node.cnt << " ";
history[s] = node.cnt;
break;
}
//扩展子状态
int x = node.x;
int nx1 = ((x + 1) % 32768);
if (!vis[nx1])
{
vis[nx1] = 1;
q.push(Node(nx1, node.cnt + 1));
}
int nx2 = ((2 * x) % 32768);
if (!vis[nx2])
{
vis[nx2] = 1;
q.push(Node(nx2, node.cnt + 1));
}
}
}
}
一开始还被hack到tle了, 用记忆化即可破hack
C. water the trees
题意:
长度为N的数组,操作k天,奇数天的时候可以选择一个数+1,偶数天选择一个数+2,也可以选择该天不操作。问最少需要多少天使所有的数都一样。
思路:
二分答案。递增性:如果操作x天可以使得所有数都一样,操作x+1天必然也可以使得所有数都一样。这点很容易证明,x天可以满足,那多一天,不管奇偶,我这天啥也不干,就能直接转化到只用了x天的情况
check函数:
经过大佬们的证明和直观感觉,其实最少的天数只出现在把所有数变成原数组中的最大值Max
或者 Max+1
,一定不是Max+2以上
- max+2为什么不可行:
举个例子,原先数是:1 1 2,max=2
[假设] 最优解是将所有数变为4,例如
1+[1+2] 1+[1+1+1] 2+[2]
那么,这个假假设不成立,因为我们可以通过倒退几天,使得每个数都同时-2(这里的本质还是因为偶数可以由奇数或者偶数构成,所以无所谓奇偶,每倒退一天都可以为每个数的-2做出contrition),因此直接将每个数变为max=2是更优的,没有必要多花功夫把每个数再多加2。
- max+1为什么可能可行:
例:1 1 1 1 1 1 1 2 2,max=2
最优方案,变为max+1=3:
1+2 1+2 1+2 1+2 1+2 1+1+1 1+1+1 2+1 2+1
(11天)
注意到,这个时候我们 没有办法 通过撤回某些操作令全体减少1(这里的本质就是奇数只能由至少一个奇数和whatever组成),假设你要倒退两天,第一天可能是奇数天,使得某个数-1,第二天必然是偶数天,只会使得某个数-2,达到不了目的
突然发现上面这种证明不全面,全体减1是可以做到的,只是需要偶数天都不动,需要2*n-1天,直接倒退回负天,但也确实说明了达到max和max+1可以相互转化,只是其实有更快的方法去达到max+1,这一点我们可以直观地感受到的,但是如果肯定没有更快到达max+2的方法,达到max+2的过程就必然要经历达到max的点,也是可以直观地想象到
已知最终情况和初始情况和花费天数,只需要check在天数内能否可行即可
check函数的核心就是:
(1)奇数天是有一个最少的限制值odd,检查是否day是否够提供这么多个奇数天,即(odd*2-1<day)
(2)检查day总共能提供多少滴水,能否超过或等于将所有元素变成Max或Max+1所需的sum值(超过是无所谓的,因为有些天可以选择不作为)
本质:“因为还剩下偶数的情况是奇数天和偶数天都能应对的,所以无所谓奇偶天,只需要算够不够,而还剩下奇数的情况,只能由奇数天来解决”
题目分奇偶天,而导致出来的特殊点就是在这里,就是剩下奇数天就必须起码要有一个奇数天站出来解决,如果是剩下偶数天,是无所谓奇偶的,典型数据就是: 1 1 1 1 1 1 2 ,这里如果要变成Max,就必须要有6个奇数天先站出来,能满足这个条件的话就直接算剩下的sum值和day值能提供的水量就行
//check函数的核心就是:
//(1)奇数天是有一个最少的限制值odd,检查是否day是否够提供这么多个奇数天,即(odd*2-1<day)
//(2)检查day总共能提供多少滴水,能否超过或等于将所有元素变成Max或Max+1所需的sum值(超过是无所谓的,因为有些天可以选择不作为)
//wa1:二分的l和r指针的初始值必须指向一个不可行解,我的l一开始设成 int l=0; 这就错了,因为0是一个可行解,如单元素的情况
//wa2:果然还是太低估了cf的数据,days最多是超过了int的,得全改成long long
//做完以后再思考了第一个check有没有意义,是不是可以去掉呢,一去掉就wa……,看到数据,豁然开朗
//1 1 1 1 1 1 2 这样的数据,如果统计加到Max的sum话,sum=6,那么只需要4天,分别1 2 1 2,就可以达到sum了
//,but这里显然是不合理的,其实本质就是,只有当你确定了已经能把所有还差奇数个的数字变成了还差偶数个后,你才能为所欲为
//才能不分奇偶的只根据还差的sum值和我能提供的水滴量作check,
//更本质的是:“因为还剩下偶数的情况是奇数天和偶数天都能应对的,所以无所谓奇偶天,只需要算够不够,而还剩下奇数的情况,只能由奇数天来解决”
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int maxN = 300000 + 5;
int arr[maxN];
int n;
int Max;
bool check(ll day);
ll sum = 0;
int odd = 0;
int main()
{
int t;
cin >> t;
while (t--)
{
Max = -1;//顺便把数组的最大元素求出
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> arr[i];
Max = max(Max, arr[i]);
}
//(1)先把 将所有元素变成Max所需的sum值 和 至少需要多少奇数天odd 求出来
sum = 0;
odd = 0;
for (int i = 1; i <= n; i++)
{
if ((Max - arr[i]) & 1) //这个元素至少需要一个奇数才能到Max
{
odd++;
}
sum += Max - arr[i];
}
//(2)第一次二分check
ll l = -1, r = 1e18;
//上限不好确定,1e9先试试,初始下限得设为-1,
//因为mid不可能取回最初的l和r,因此我们的初始值必须是一个不可行解,而0是一个可行解,如单元素的情况
while (l + 1 != r)
{
ll mid = l + ((r - l) >> 1);
if (check(mid))//该天可以满足
{
r = mid;
}
else
{
l = mid;
}
}
ll res = r;//记录结果,最小可满足的天
//(3)再把 将所有元素变成Max+1所需的sum值 和 至少需要多个奇数天odd求出来
sum = 0;
odd = 0;
for (int i = 1; i <= n; i++)
{
if ((Max + 1 - arr[i]) & 1) //这个元素至少需要一个奇数才能到Max
{
odd++;
}
sum += Max + 1 - arr[i];
}
//(4)第二次check(两次分别是check能否到max和max+1)
l = -1, r = 1e18;
while (l + 1 != r)
{
ll mid = l + ((r - l) >> 1);
if (check(mid))//该天可以满足
{
r = mid;
}
else
{
l = mid;
}
}
res = min(res, r);//取两次check中小的那次
cout << res << endl;
}
}
//check能否到用day天把所有变成原数组的max或max+1(体现在sum值!!!)
bool check(ll day)
{
//1.check奇数天够不够用(只要起码满足了最少的奇数天,剩下要检查的就是能否凑够sum值了)
if (day < 2 * odd - 1)
{
return false;
}
//2.check在day个天内,能否凑够sum值(超出无所谓,因为可以选择一天啥也不做)
if (day & 1)//奇数天
{
return sum <= (day / 2 + 1) + (day / 2) * 2; //奇数天*1+偶数天*2
}
else//偶数天
{
return sum <= (day / 2) * 3; //奇数天*1+偶数天*2
}
}