2023.8.15模拟赛题解
模拟赛题解
P7382[COCI2018-2019#6] Simfonija
转化题意,就是钦定一个整数 \(x\),选定 \(n-k\) 个 \(a_i\) 和 \(b_i\),令 \(\sum_{j=1}^{n-k}|a_j+x-b_j|\) 最小。因此,我们只要钦定出最优方案下的 \(x\),选定对于当前 \(x\) 前 \(n-k\) 小的 \(|a_i+x-b_i|\) 即可。
思路
首先,\(a_i-b_i\) 是定值,设 \(c_i=a_i-b_i\)。钦定一个 \(x\),如果令答案最小,发现选定的所有 \(c_j\) 一定是在 \(x\) 两侧连续排布。于是对 \(c\) 排序,保证选定的所有 \(c_j\) 是连续的,枚举所有长度为 \(n-k\) 的连续子区间即可。
如果直接枚举求的话时间复杂度为 \(O(n^2)\),需要用前缀和优化成 \(O(n)\)。对于用前缀和求中位数与每个值的差的绝对值之和的最小值是很有 trick 意味在里面的。思路来源于这道题以及这篇题解。
假设中位数为 \(x\),分两种情况讨论。
-
\(r-l\) 是奇数,求的是 \(\min\{\sum_{i=l}^{r}|c_i+x|\}\)。
比 \(x\) 小的数有 \(c_l,c_{l+1},...c_{mid}\),比 \(x\) 大的数就有 \(c_{mid+1},c_{mid+2},...,c_r\),其中 \(mid=\frac{l+r}{2}\)。于是就可以转化成求 \(\sum_{i=l}^{mid}|c_i+x|\) 和 \(\sum_{i=mid+1}^r|c_i+x|\) 的和的最小值。
我们取出两个式子中的两边一项 \(c_l\) 和 \(c_r\),求 \(\min\{|c_l+x|+|c_r+x|\}\),那么一定满足 \(-c_r\le x\le -c_l\)。
设 $ c_r\ge c_l\ge 0$ 则有 \(\min\{|c_l+x|+|c_r+x|\}=x-c_l+c_r-x=c_r-c_l\)。同理选出 \(c_{l+1}\) 和 \(c_{r-1}\),也能得出类似的结论...因此最小值为 \(\sum_{i=mid+1}^rc_i-\sum_{i=1}^{mid}c_i\)。
-
同理如果 \(r-l\) 是偶数,可以推得所求的值就是 \(\sum_{i=mid+1}^{r}-\sum_{i=l}^{mid-1}\)。
总复杂度为 \(O(n\log n)\),瓶颈在排序。
代码很简单,就不贴了,应该也比较好写。
P6346[CCO2017]专业网络
思路
一个显然的思路是:直接根据 \(a\) 排序,如果能免费交的朋友就直接交,如果必须花钱结交,那么就贪心的选择还没有结交的朋友中花费较小的来结交,但是会直接被第三个样例 hack 掉。
尝试改进这个思路,根据正难则反,我们可以从后往前考虑前面所有人都是花钱购买的这种前提下,当前这个人是否仍需花钱购买。
如果前面所有人加上后面必须购买的人的总和仍然小于当前这个人的 \(a_i\),说明当前这个人暂时不能免费结交。我们有两种方式让他与我们结交:
- 直接购买他
- 购买那些本来可以免费结交的人,使得他可以免费结交。
最优方法一定是购买所有满足以上两种情况之一的人中,花钱最少的那个,这样当前这个人就可以结交了。这个过程可以用堆实现。
这种策略的好处是一直保持在一个既花钱少又能保持人数多的状态,如果这种状态仍然小于当前这个人的要求 \(a_i\),就只能花钱通过两种方式之一才能邀请到当前这个人加入。通过从后往前不断地调整,就可以得到当前的最优解。
代码略过,很好写。
经验题
CF1251E1 Voting (Easy Version)
CF1251E2 Voting (Hard Version)
P5999 [CEOI2016] kangaroo
清奇的 dp 题,总之是好题,参考了 Mfeitveer 大佬的题解。
思路
将袋鼠每次跳的过程抽象一下,就变成若干个连通块通过增加和合并最终变成一块。
对于本题只有这两种操作,所以就不难设计状态。令 \(f_{i,j}\) 表示当前放入了 \(i\) 个元素,拼凑成 \(j\) 块的方案数,最终答案就是 \(f_{n,1}\)。如果新加入一个元素,有两种操作:
- 将当前元素单独作为新的一块,那么除了 \(s\) 的左侧和 \(t\) 的右侧都可以放,设合法方案数为 \(k\),则有 \(f_{i-1,j-1}\times k \to f_{i,j}\)
- 合并其中两个连续块为一块,选定两个联通块都可以合并,原有 \(j+1\) 个块,因此方案数为 \(j\),则有 \(f_{i+1,j-1}\times j\to f_{i,j}\)
注意需要特判 \(i=s\) 和 \(i=t\) 两种情况。对于操作 \(1\),当 \(i=s\) 时,只能放在最左侧,当 \(i=t\) 时,只能放在最右侧。对于操作 \(2\),两者均只有一种合并方式。
代码
#include<iostream>
using namespace std;
typedef long long ll;
const int N=2010,mod=1e9+7;
int n,s,t;
ll f[N][N];
int main()
{
cin>>n>>s>>t;
f[1][1]=1;
for(int i=2;i<=n;i++)
for(int j=1;j<=n;j++)
{
if(i==s||i==t)
{
(f[i][j]+=f[i-1][j-1]+f[i-1][j])%=mod;
continue;
}
int res=(i>s)+(i>t);
(f[i][j]+=f[i-1][j-1]*(j-res)+f[i-1][j+1]*j)%=mod;
}
cout<<f[n][1];
return 0;
}
其他类似题目
CF704B Ant Man
P9197[JOI Open 2016]摩天大楼
P5967[POI2016]Korale
参考 zzz 学长的博客
简单思路
分成两个问题解决:
- 求第 \(k\) 小值
- 求第 \(k\) 小值对应的集合
对于第一问:
由于我们只是求第 \(k\) 小值,因此可以直接忽略编号大小带来的影响。
首先按照权值排序。设这个二元组为 \((sum,i)\),表示前 \(i-1\) 个数选出若干数与第 \(i\) 个数的和为 \(sum\),先将排序后的第一个数放入小根堆中,每次取出最小的一个,然后把 \((sum+a_{i+1},i+1)\) 和 \((sum+a_{i+1}-a_i,i+1)\) 放入堆中,学长说这种方式可以不重不漏,但是蒟蒻太弱了,不知道为什么,有没有大佬知道的给蒟蒻讲讲呗。
对于第二位
设上一问求得的答案为 \(ans\),排名小于等于 \(k\) 且和为 \(ans\) 的数的个数为 \(cnt\)。直接从前往后 dfs 取靠前的数,每次取的数要保证是区间最小值,这个东西可以用线段树在线维护,或者 st 表离线求也可以。
两部分时间复杂度都是 \(O(k\log n)\),因此总复杂度就是 \(O(k\log n)\)。
代码
不开 \(O_2\) 应该过不了,反正我没过去。
#include<iostream>
#include<queue>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=1e6+10;
int n,k,a[N],b[N];
typedef pair<ll,int>PLI;
#define x first
#define y second
priority_queue<PLI,vector<PLI>,greater<PLI>>q;
ll ans,cnt;
int read()
{
int x=0;
char ch=getchar();
while(!isdigit(ch))ch=getchar();
while(isdigit(ch))x=x*10+(ch^48),ch=getchar();
return x;
}
void write(ll x)
{
if(x>9)write(x/10);
putchar(x%10+'0');
}
int mn[N<<2];
#define ls u<<1
#define rs u<<1|1
void build(int u,int l,int r)
{
if(l==r)return mn[u]=a[l],void();
int mid=(l+r)>>1;
build(ls,l,mid);
build(rs,mid+1,r);
mn[u]=min(mn[ls],mn[rs]);
}
int query(int u,int l,int r,int pl,ll x)
{
if(pl<=l)
{
if(mn[u]>x)return 0;
if(l==r)return l;
}
int mid=(l+r)>>1;
if(pl<=mid)
{
int t=query(ls,l,mid,pl,x);
if(t)return t;
}
return query(rs,mid+1,r,pl,x);
}
int top,stk[N];
void dfs(int l,ll res)
{
if(!res&&!--cnt)
{
write(ans),putchar('\n');
for(int i=1;i<=top;i++)write(stk[i]),putchar(' ');
exit(0);
}
for(int i=l+1;i<=n;i++)
{
i=query(1,1,n,i,res);
if(!i)return;
stk[++top]=i;
dfs(i,res-a[i]);
top--;
}
}
int main()
{
n=read(),k=read()-1;
if(!k)return puts("0"),0;
for(int i=1;i<=n;i++)b[i]=a[i]=read();
sort(b+1,b+1+n);
q.push({b[1],1});
for(int i=1;i<=k;i++)
{
PLI t=q.top();q.pop();
if(t.x==ans)cnt++;
else ans=t.x,cnt=1;
if(t.y==n)continue;
t.x+=b[++t.y];
q.push(t);
t.x-=b[t.y-1];
q.push(t);
}
build(1,1,n);
dfs(0,ans);
return 0;
}
后记:感觉这道题还是很有价值和难度的,自己也没有完全理解透彻,因此看到这里的大佬们最好也只做个参考。有锅我背,有您的高见我不胜感激。