A题:Array Balancing(迷惑性很强)(GOOD)
- 迷惑性强:
- 下界:每一对取 m i n { ∣ a [ i ] − a [ i − 1 ] ∣ + ∣ b [ i ] − b [ i − 1 ] ∣ , ∣ a [ i ] − b [ i − 1 ] ∣ + ∣ b [ i ] − a [ i − 1 ] ∣ } min\{|a[i]-a[i-1]|+|b[i]-b[i-1]|,|a[i]-b[i-1]|+|b[i]-a[i-1]|\} min{∣a[i]−a[i−1]∣+∣b[i]−b[i−1]∣,∣a[i]−b[i−1]∣+∣b[i]−a[i−1]∣}
- 一定可以构造出该下界:后面只需看前面i-1是否交换。
- 或考察最优解的性质(贪心选择):相邻之间的差和一定最小。如果不这样统一交换后面所有得到更优的解。
- 最优子结构(显然)
int main()
{
int t;cin>>t;
for(int i = 0;i<t;i++)
{
int n;cin>>n;
int a[n];
int b[n];
for(int k = 0;k<n;k++) cin>>a[k];
for(int k = 0;k<n;k++) cin>>b[k];
ll ans = 0;
for(int k = 1;k<n;k++)
{
int c = min(abs(a[k]-a[k-1])+abs(b[k]-b[k-1]),abs(a[k]-b[k-1])+abs(b[k]-a[k-1]));
ans+=(ll)c;
}
cout<<ans<<endl;
}
system("pause");
}
B题-Getting Zero
思路
- 正向BFS找最短路困难,考虑反向BFS找最短路(避免重复从开始结点搜索)。
- 根据题目条件
可以得出目标到开始结点每一条边什么时候存在。
算法分析:每个结点至多入队一次,出队一次。循环总公运行32768次。
void solve(int dist[])
{
dist[0]=0;
queue<int> q; q.push(0);
int count = 0;
while(!q.empty()) //循环至多运行32768次
{
count++;
int v = q.front();
q.pop();
if(v%2==0&&dist[v/2]==-1) dist[v/2]=dist[v]+1,q.push(v/2);
if(v%2==0&&dist[(v+32768)/2]==-1) dist[(v+32768)/2]=dist[v]+1,q.push((v+32768)/2);
if(dist[(v+32767)%32768]==-1) dist[(v+32767)%32768]=dist[v]+1,q.push((v+32767)%32768);
}
cout<<count<<endl;
}
int main()
{
int dis[32768];
memset(dis,-1,32768*sizeof(int));
solve(dis);
int n;cin>>n;
for(int j = 0;j<n;j++)
{
int a;
cin >> a;
cout<<dis[a];
if(j<n-1) cout<<" ";
}
system("pause");
}
C题 Water the Trees
思路
- 只需搜索 m a x h e i g h t maxheight maxheight和 m a x h e i g h t + 1 maxheight+1 maxheight+1的情况。是因为只需要把奇数用1弥补(必要条件也是充分条件)
- 二分查找最小天数即可。
int main()
{
int t;
cin>>t;
for(int i = 0;i<t;i++)
{
int n;cin>>n;
int h[n];
for(int k =0;k<n;k++) cin>>h[k];
ll sum = 0;
ll maxm = -1;
ll c_odd = 0; //需要的奇数
ll c_even = 0; //需要的偶数
for(int k = 0;k<n;k++)
{
sum+=(ll)h[k];
maxm=max(maxm,(ll)h[k]);
if(h[k]%2==0) c_even++;
else c_odd++;
}
//同样高度为maxm
ll remain = maxm*(ll)n-sum;
if(maxm%2!=0)
{
ll temp = c_odd;c_odd=c_even;c_even=temp;
}
ll low = 0,high = 2*remain+1;
while(low<high)
{
ll mid = (low+high)>>1;
if(!(mid-mid/2>=c_odd&&(mid/2*3+(mid%2)) >= remain)) low=mid+1; //如果不满足条件
else high=mid;
}
ll ans = low;
ll temp = c_odd;c_odd=c_even;c_even=temp;
remain = (maxm+1)*(ll)n-sum;
low = 0,high = 2*remain+1;
while(low<high)
{
ll mid = (low+high)>>1;
if(!(mid-mid/2>=c_odd&&(mid/2*3+(mid%2))>=remain)) low=mid+1; //如果不满足条件
else high=mid;
}
ans = min(ans,low);
cout<<ans<<endl;
}
system("pause");
}
D题Progressions Covering![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/63f8b1678ba265c118d1951e37b12699.png)
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/fb08d777db1d2cc255caefd20e71a0a7.png)
思路
- 从最后一个点往前贪心
- 关注当前窗口何时终止(以curs[i]记录),以维护每个点应该被减去的值predec
#include<iostream>
#include<vector>
#include<string>
#include<set>
#include<algorithm>
#include<map>
#include<queue>
#include <chrono>
#include<math.h>
#include<unordered_map>
#include <bits/stdc++.h>
using namespace std;
using namespace std;
const int N = 1e5+5;
const int S = 500;
const long long mod = 1e9+7;
typedef long long ll;
int main()
{
int n,k; cin>>n>>k;
ll b[n];
for(int i = 0;i<n;i++) cin>>b[i];
int curs[n]; //在何时终止减去该值
memset(curs,-1,n*sizeof(int));
ll dec[n];
memset(dec,0,n*sizeof(ll));
ll sum = 0; //作为等差数列应该减去的值
ll pre_dec = 0; //前一个数字减去的值
ll ans = 0;
for(int i = n-1;i>=0;i--)
{
ll c = b[i];
pre_dec -= sum; //当前数值应减去的数字
if(c - pre_dec > 0)
{
ll c_c = (ll)ceil((double)(c - pre_dec)/(double)(min(k,i+1)));
ans+=c_c;
sum+=c_c;
dec[i]=c_c;
pre_dec += c_c * min(k,i+1);
int index = max(0,i-k+1);
curs[index] = i; //在index停止减去
}
if(curs[i]!=-1) {sum-=dec[curs[i]];pre_dec -=dec[curs[i]];} //下一个数不该此等差数列中的结果
}
cout << ans << endl;
system("pause");
return 0;
}
E题(Narrow Components)
思路
我的思路(BAD)
- 维护每个前缀连通分量的个数
- 分类讨论。尤其需要注意一列中中间堵住,两边不堵住的情况,连通分量个数有可能会减少
#include<iostream>
#include<vector>
#include<string>
#include<set>
#include<algorithm>
#include<map>
#include<queue>
#include <chrono>
#include<math.h>
#include<unordered_map>
#include <bits/stdc++.h>
using namespace std;
using namespace std;
const int N = 1e5+5;
const int S = 500;
const long long mod = 1e9+7;
typedef long long ll;
int getpre(int l,int r,int pre[])
{
if(l==0) return pre[r];
return pre[r]-pre[l-1];
}
int main()
{
int n;
cin >> n;
string a[3];
cin >> a[0]; cin>>a[1]; cin>>a[2];
//1 free cell,0 taken cell
int pre1[n],pre2[n];
pre1[0] = a[0][0]-'0'; pre2[0] = a[2][0]-'0';
for(int i = 1;i<n;i++) {pre1[i] = pre1[i-1]+(a[0][i]-'0');pre2[i] = pre2[i-1]+(a[2][i] - '0');}
//一列三个都是空闲的下标
vector<int> em;
for(int i = 0;i<n;i++)
{
if(a[0][i] == '1'&&a[1][i] == '1'&&a[2][i] == '1') em.push_back(i);
}
int pref[n]; //前缀连通分量个数
if(a[1][0] == '0') pref[0]=(a[0][0]-'0') + (a[2][0] - '0'); else pref[0]=1;
for(int i = 1;i<n;i++) //
{
if(a[1][i] == '0') //中间堵住
{
pref[i]=pref[i-1]+((!(a[0][i-1]-'0') && (a[0][i]-'0'))+(!(a[2][i-1]-'0') && (a[2][i] - '0')));
}
else //中间没有堵住
{
if(a[1][i-1]=='1') pref[i]=pref[i-1];
else
{
if(a[2][i]=='0') //下面的堵住
{
if(a[0][i]=='0' || a[0][i]=='1' && a[0][i-1]=='0') pref[i]=pref[i-1]+1;
else pref[i]=pref[i-1];
}
else if(a[0][i] == '0') //上面的堵住
{
if(a[2][i]=='0' || a[2][i]=='1' && a[2][i-1]=='0') pref[i]=pref[i-1]+1;
else pref[i]=pref[i-1];
}
else //三个都没堵住
{
if(a[0][i-1]=='0'&&a[2][i-1]=='0') pref[i]=pref[i-1]+1;
else if(a[0][i-1]=='1'&&a[2][i-1]=='1')
{
//搜索最后一个小于i-1下标的em
int l = 0,r = em.size();
while(l<r)
{
int m = (l+r)>>1;
if(em[m]<i-1) l=m+1; else r = m;
}
l--;
if(l>=0&&l<em.size()&&getpre(em[l],i,pre1)==i-em[l]+1&&getpre(em[l],i,pre2)==i-em[l]+1)
{
pref[i]=pref[i-1];
}
else pref[i]=pref[i-1]-1; //本来就存在于一个CC中
}
else pref[i]=pref[i-1];
}
}
}
}
int q;cin>>q;
for(int i = 0;i<q;i++)
{
int l,r;cin>>l>>r;
l--;r--;
if(l==0) cout<<pref[r]<<endl;
else
{
if(a[0][l] == '0' && a[1][l] == '0' && a[2][l] == '0') cout << pref[r]-pref[l]<<endl;
else
{
if(a[1][l] == '1') cout<<pref[r]-pref[l]+1<<endl;
else //l中间的被堵住
{
if(a[0][l]=='1'&&a[2][l]=='1')
{
int l1 = 0,r1 = em.size(),l2=0,r2 = em.size();
//找最后一个小于l的
while(l1<r1)
{
int m = (l1+r1)>>1;
if(em[m]<l) l1=m+1; else r1 = m;
}
l1--;
while(l2<r2)
{
int m = (l2+r2)>>1;
if(em[m]<=l) l2=m+1; else r2=m;
}
bool flag1 = false,flag2 = false; //前缀中是否位于同一个连通分量
if(l1>=0&&l1<em.size()&&getpre(em[l1],l,pre1)==l-em[l1]+1&&getpre(em[l1],l,pre2)==l-em[l1]+1)
flag1 = true;
if(l2>=0&&l2<em.size()&&em[l2]<=r&&getpre(l,em[l2],pre1)==em[l2]-l+1&&getpre(l,em[l2],pre2)==em[l2]-l+1)
flag2 = true;
if(flag1&&flag2)cout<<pref[r]-pref[l]+1<<endl;
else cout<<pref[r]-pref[l]+2<<endl;
}
else cout<<pref[r]-pref[l]+1<<endl;
}
}
}
}
system("pause");
}
官方解法(待补充)
F题 (Teleporters)GOOD 思维量极大
思路
- 若固定点 b 1 , b 2 , ⋯ b n b_1,b_2,\cdots b_n b1,b2,⋯bn,一定是从 b 1 b_1 b1跳到 b 2 b_2 b2,从 b 2 b_2 b2跳到 b 3 b_3 b3…从 b n − 1 b_{n-1} bn−1跳到 b n b_n bn.以此类推。否则会产生多余的交叉项。
错误的贪心策略
- 每次选择最大的段长度。选择其中点的位置作为添加点
- 似乎每次能量减少得最多
- 但是不能证明贪心选择得正确性(即不能证明一定存在一个最优解包含该种选择)
正解
最优解满足的条件
- 最优解符合如下形式:若在 0 − a 1 0-a_1 0−a1间插入 k 1 k_1 k1个点,在 a 1 − a 2 a_1-a_2 a1−a2之间插入 k 2 k_2 k2个点,在 a 2 − a 3 a_2-a_3 a2−a3之间插入 k 3 k_3 k3个点…以此类推。对每个区间而言,其插入点后形成的段都会是大致等间距的。即对每个区间 a i − 1 ∼ a i a_{i-1}\sim a_i ai−1∼ai,插入点后划分的段长度至多相差1.要么为 ⌊ a i − a i − 1 k i + 1 ⌋ \lfloor \frac{a_i - a_{i-1}}{k_i +1} \rfloor ⌊ki+1ai−ai−1⌋,要么为 ⌈ a i − a i − 1 k i + 1 ⌉ \lceil \frac{a_i - a_{i-1}}{k_i +1} \rceil ⌈ki+1ai−ai−1⌉。
在满足最优解条件的情况下搜索最优解
-
设 f ( x , k ) f(x,k) f(x,k)为将长度为x的区间,使用k个中间点划分(即形成k+1段)后,从该区间开始传递到末尾需要的能量。由于需考虑对单个区间增加一个点后的变化情况,因此考虑增加点后该区间能量的变化 f ( x , k ) − f ( x , k + 1 ) f(x,k) - f(x,k+1) f(x,k)−f(x,k+1).关键步骤:
f ( x , k ) − f ( x , k + 1 ) ≥ f ( x , k + 1 ) − f ( x , k + 2 ) f(x,k) - f(x,k+1) \geq f(x,k+1) - f(x,k+2) f(x,k)−f(x,k+1)≥f(x,k+1)−f(x,k+2)
当允许每个划分的段的区间长度不是整数时,该结论很容易证明。说明对于任意 a i ∼ a i + 1 a_i \sim a_{i+1} ai∼ai+1区间而言,每增加一个点带来的增量大小是单调不增的
如图所示,每个单元格定义为在当前区间添加一个点后,该区间总能量(亦即总能量)的变化情况。可将每一列视作一个降序数组。问题转换为将每个降序数组进行由大到小归并后,取最少的数使其和达到给定的阈值。在本问题中,该阈值就是能量的减少量。
-
如图所示:由此带来一种类似归并排序的思路:遍历所有 a i , a i + 1 a_i,a_{i+1} ai,ai+1区间,并设当前区间已经有 k i k_i ki个点,每次选择使得 f ( a i + 1 − a i , k i ) − f ( a i + 1 − a i , k i + 1 ) f(a_{i+1}-a_{i},k_i) - f(a_{i+1}-a_{i},k_i+1) f(ai+1−ai,ki)−f(ai+1−ai,ki+1)值最大对应的区间, 在其上面加上一个点,并重新计算该区间的能量。并维护能量总和,直到能量总和小于等于阈值m为止。
- 但是复杂度还不可以接受
- 我们还忽略了什么信息?
-
如果在上述问题中,如果我们还知道每个数组的前缀和(或在常数时间内获取前缀和),可以通过二分套二分的思路进行优化。
- 最优解满足:所有数组中取到的值都大于等于某个阈值 C C C。因此考虑二分这个阈值C。
- 对于固定的阈值C,对每一列由于可以常数获取上述表格中每一列的前缀和,因此可以通过 O ( n l o g L ) O(nlogL) O(nlogL)判定所有选择的元素和是否满足条件。
- 当二分得到这个阈值C后,由于C+1不满足条件,C是满足条件的最大数,所有数组中取到的数都需要严格大于等于C,且每个数组中大于等于C的元素都被取走。因此还需要根据m 去除一些严格等于C的元素,以使得最终选择的元素个数最少。
#include<iostream>
#include<vector>
#include<string>
#include<set>
#include<algorithm>
#include<map>
#include<queue>
#include <chrono>
#include<math.h>
#include<unordered_map>
#include <bits/stdc++.h>
using namespace std;
using namespace std;
const int N = 1e5+5;
const int S = 500;
const long long mod = 1e9+7;
typedef long long ll;
ll eval(ll split_num,ll length) //将length分解成split_num部分后的能量
{
ll each = length / split_num;
ll pl_1_num = length - each * split_num;
return (each*each)*(split_num - pl_1_num) + (each + 1)*(each + 1)*pl_1_num;
}
bool judge(ll b[],int n,ll m,ll C)
{
//将每一段都有f(k)-f(k-1)>=C时,是否能满足所使用的能量总和小于等于m
ll sum = 0;
for(int i = 0;i<n;i++) //对每段二分查找最后一个>=C的k值
{
if(b[i] == 1) {sum += b[i]*b[i];continue;}; //不可分隔
ll l = 1,r = b[i]; //加入的点的个数
//找第一个小于C的
while(l < r)
{
ll mid = (l + r) >> 1;
if(eval(mid,b[i])- eval(mid+1,b[i]) >= C) l = mid + 1; //加入l个点分成l+1段
else r = mid;
}
//l--; //指向最后一个大于等于C的
sum += eval(l,b[i]);
}
return sum <= m;
}
int main()
{
int n;
cin >> n;
ll a[n+1];
a[0]=0;
for(int i =1;i<n+1;i++) cin>>a[i];
ll b[n];
for(int i = 1;i<n+1;i++)
{
b[i-1] = a[i] - a[i-1];
}
ll m;
cin >> m;
//外层搜索C
ll l = 0,r = 1e18+10;
while(l<r)
{
ll mid = (l+r)>>1;
if(judge(b,n,m,mid)) l = mid+1;
else r = mid;
} //l=r指向第一个不能满足条件的C
ll sum = 0;
ll r1 = --r; //最后一个满足条件的C
ll ans = 0;
for(int i = 0;i<n;i++)
{
if(b[i] == 1) {sum += b[i]*b[i];continue;}; //不可分隔
ll l = 1,r = b[i]; //加入的点的个数
//找第一个小于C的
while(l < r)
{
ll mid = (l + r) >> 1;
if(eval(mid,b[i])-eval(mid+1,b[i]) >= r1) l = mid + 1; //加入l个点分成l+1段
else r = mid;
}
//l--; //指向最后一个大于等于C的
sum += eval(l,b[i]);
ans += (l - 1);
}
cout << ans - (m - sum)/r1 << endl; //需要根据m **去除一些严格等于C的元素**,
system("pause");
return 0;
}