ICPC2021昆明区域赛题解
The 2021 ICPC Asia Kunming Regional Contest
特地挑的差评最多的一场来打,但是怎么说呢,感觉题目思维都挺有意思的。可能是 A 题大模拟实在是太难评了吧,然后这篇题解也没写 A 题。
ABCDEFGHIJKLM
K. King of Gamers 数学
给一个分数 \(x=\frac{a}{b}\),一个人玩 \(n\) 轮游戏,每一轮时,如果他的当前胜率小于等于 \(x\),则获胜一场,如果当前胜率大于 \(x\),则这一轮不获胜,问最后一共获胜多少轮。\(T\) 组数据。
\(1\leq a\leq b\leq 10^9,n\leq 10^9,T\leq 10^5\)
答案是
感觉挺对,证明的话懒得写了对不起。
#include <bits/stdc++.h>
typedef long long ll;
ll a, b, n;
void work() {
scanf("%lld%lld%lld", &n, &a, &b);
ll y = a * n / b;
if (y * b <= a * (n - 1))
y ++;
printf("%lld\n", y);
}
int main() {
int T; scanf("%d", &T);
while (T --)
work();
return 0;
}
B. Blocks 动态规划
给 \(n\) 个在平面上的矩形,第 \(i\) 个矩形的左下角是 \((x_{1,i},y_{1,i})\),右上角是 \((x_{2,i},y_{2,i})\)。每次会随机选择一个矩形,并将它所覆盖的面积涂色,问要将 \((0,0)\) 到 \((W,H)\) 的这个大矩形全部涂色,所要涂色的期望次数。报告期望次数对 \(998244353\) 取模或不存在方案。
\(T\leq 500,n\leq 10,1\leq W,H\leq 10^9,0\leq x_{1,i}<x_{2,i}\leq 10^9,0\leq y_{1,i}<y_{2,i}\leq 10^9\)。
由于矩形的数量很少,已经涂色的矩形集合可以用二进制状态 \(S\) 表示,我们可以判断 \(S\) 是否覆盖了大矩形。
定义 \(f(S)\) 表示当前已经涂色的集合为 \(S\),还需要涂多少次色才能覆盖完大矩形。当 \(S\) 已经覆盖完时,\(f(S)=0\)。对于其余 \(S\),可以得到转移:
从大到小枚举 \(S\) 即可。
现在考虑如果找到 \(f(S)=0\) 的状态,由于 \(n\) 很小,方法很多。笨人是使用类似扫描线的方法,求每个离散的横坐标上此时已经覆盖的纵坐标的集合,如果某个横坐标上已覆盖纵坐标的集合不能完全包含 \([0,H]\),就非法。这个可以使用差分再前缀和的方法做,具体见代码吧(很丑)。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int>ttfa;
const ll P=998244353;
namespace binom{
const ll MOD=998244353;
const int RANGE=2000000;
inline ll fpr(ll b,ll t=MOD-2,ll x=1ll){
for(;t;t>>=1,b=b*b%MOD)
if(t&1)x=x*b%MOD;
return x;
}
ll bin_mul[RANGE+5],bin_inv[RANGE+5];
inline void binom_init(){
bin_mul[0]=1;
for(int i=1;i<=RANGE;++i)bin_mul[i]=bin_mul[i-1]*i%MOD;
bin_inv[RANGE]=fpr(bin_mul[RANGE]);
for(int i=RANGE-1;i>=0;--i)bin_inv[i]=bin_inv[i+1]*(i+1)%MOD;
}
inline ll C(ll n,ll m){
if(m>n){puts("binom range wrong");exit(0);}
return bin_mul[n]*bin_inv[m]%MOD*bin_inv[n-m]%MOD;
}
}
using namespace binom;
const int N=15;
const ll INF=0x3f3f3f3f3f3f3f3f;
int n,W,H,val[1<<N];
array<int,4>a[N];
struct node{
int x,l,r,f;
}lin[4*N];int tot=0;
multiset<ttfa>si;
bool cmp(node x,node y){
if(x.x==y.x)return x.f>y.f;
return x.x<y.x;
}
int calc(){
si.clear();
sort(lin+1,lin+1+tot,cmp);
if(0<lin[1].x)return 0;
for(int i=1;i<=tot;){
int x=lin[i].x;
for(;lin[i].x==x&&i<=tot;++i){
if(lin[i].f==1){
si.insert({lin[i].l,0});//0 add
si.insert({lin[i].r,1});//1 del
}else{
si.erase(si.find({lin[i].l,0}));
si.erase(si.find({lin[i].r,1}));
}
}
if(x>=W)break;
if(si.empty()){
if(0<=x&&x<W)return 0;
continue;
}
if(0<si.begin()->first)return 0;
int cnt=0;
for(auto [y,f]:si){
if(f==0)++cnt;
else --cnt;
if(cnt<0)assert(0);
if(0<=y&&y<H&&cnt==0)return 0;
}
}
return 1;
}
ll f[1<<N];
int main(){
int T;scanf("%d",&T);
while(T--){
scanf("%d%d%d",&n,&W,&H);
for(int i=0;i<n;++i){
scanf("%d%d%d%d",&a[i][0],&a[i][1],&a[i][2],&a[i][3]);
}
for(int i=0;i<(1<<n);++i)val[i]=f[i]=0;
for(int i=1;i<(1<<n);++i){
tot=0;
for(int j=0;j<n;++j){
if((i>>j)&1){
lin[++tot]=(node){a[j][0],a[j][1],a[j][3],1};
lin[++tot]=(node){a[j][2],a[j][1],a[j][3],0};
}
}
val[i]=calc();
}
if(val[(1<<n)-1]==0){
puts("-1");
continue;
}
for(int i=(1<<n)-1;i>=0;--i){
if(val[i]==1){f[i]=0;continue;}
ll tmp=0,cnt=__builtin_popcount(i);
for(int j=0;j<n;++j){
if((i>>j)&1)continue;
tmp=(tmp+f[i+(1<<j)])%P;
}
tmp=(tmp*fpr(n)%P+1ll)%P;
f[i]=tmp*n%P*fpr(n-cnt)%P;
}
printf("%lld\n",f[0]);
}
return 0;
}
F. Find the Maximum 最优化
给一个 \(n\) 个点的无向树,树上点有点权 \(b_i\),现在你需要找到一个至少 \(2\) 个点的树上路径和一个实数 \(x\),假设 \(V\) 是这个路径上的点集,使得 \(\frac{\sum_{u\in V}(-x^2+b_ux)}{|V|}\) 这个式子有最大值,给出这个最大值。
\(1\leq n\leq 10^5,|b_i|\leq 10^5\)。
假设路径起点终点为 \(u,v\),\(b(u,v)\) 为 \(u\) 到 \(v\) 路径权值的平均值,变成求 \(-x^2+b(u,v)x\) 的最大值。
根据二次函数的性质,当 \(x=\frac{b(u,v)}{2}\) 时,有最大值 \(\frac{b(u,v)^2}{2}\),故要求 \(b(u,v)\) 的最大值和最小值。求出最大值后最小值可以将 \(b\) 取相反数后按同样的方法求。
赛时队友二分了路径的平均值,具体见下面的代码吧。
然后题解有结论
一定存在一条长度不大于 \(3\) 的路径可以取到最大值。
证明:
假设有一条长度大于等于 \(4\) 的路径,取前 \(2\) 个元素为一条子路径,其余元素为另一条子路径,由平均数,原路径的平均权值一定介于这两条子路径之间,故可以选择其中一条子路径,这条子路径的权值大于等于原权值且长度大于等于 \(2\)。
于是枚举每个点相邻的两个点或一个点即可求出最大平均值。
const int MAXN = 1e5+5;
const double eps = 1e-10;
int n;
double val[MAXN], ans[MAXN], dp[MAXN];
vector <int> e[MAXN];
void calc(int u, int fa, double mid) {
dp[u] = val[u]-mid;
ans[u] = -1e13;
for(auto v: e[u]) if(v^fa) {
calc(v, u, mid);
ans[u] = max(ans[u], max(dp[u]+dp[v], ans[v]));
dp[u] = max(dp[u], dp[v]+val[u]-mid);
}
}
bool check(double mid) {
calc(1, 0, mid);
return ans[1] > eps;
}
int main() {
// freopen("r.in","r",stdin);
n = read(9);
for(int i=1; i<=n; ++i) val[i]=read(9);
for(int i=1; i<n; ++i) {
int u=read(9), v=read(9);
e[u].push_back(v);
e[v].push_back(u);
}
double l=-MAXN, r=MAXN, mid;
while(r-l > eps) {
mid = (l+r)/2;
if(check(mid)) l=mid;
else r=mid;
}
double res = l;
l=-MAXN, r=MAXN;
for(int i=1; i<=n; ++i) val[i]=-val[i];
while(r-l > eps) {
mid = (l+r)/2;
if(check(mid)) l=mid;
else r=mid;
}
res = max(res, l);
printf("%.8f\n", res*res/4);
return 0;
}
D. Divisions 构造
对于一个长为 \(n\) 的正整数序列 \(S\),Nami 想将它划分为两个子序列。
一开始 Nami 有两个空序列 \(S_A,S_B\),它会按顺序考虑 \(S\) 中每一个元素,要么将他放在 \(S_A\) 的末尾,要么放在 \(S_B\) 的末尾,则共有 \(2^n\) 种放法。但如果 \(S_A,S_B\) 满足
- \(S_{A,1}\leq S_{A,2}\leq \cdots \leq S_{A,n_A}\)
- \(S_{B,1}\geq S_{B,2}\geq \cdots\geq S_{B,n_B}\)
则是一种合法划分。
现在给你一个整数 \(k(0\leq k\leq 10^8)\),请你构造出一个正整数序列 \(S(0\leq S_i\leq 10^8)\),这个序列的合法划分方案恰好有 \(k\) 种。且 \(S\) 的长度 \(n\) 满足 \(1\leq n\leq 365\)。
首先注意审题,最后不同的放法可能产生的两个序列是相同的(加入所有元素都相同)。
考虑对于这样的序列 S=[1,2,2,3,3,3,4,4,4,4],即 \(S\) 是不降的序列,那么 \(S_B\) 中只能放大小相同的元素,假设放的元素是 \(x\),这种元素在原序列中有 \(num_x\) 个,则方法有 \(2^{num_x}-1\) 种(还需减去空序列),最后的答案就是 \(1+\sum_{x\in S} (2^{num_x}-1)\),还要加上 \(B\) 为空序列。
利用这个 \(2\) 的多少次方就可以对 \(k\) 进行二进制拆分了,实现很多。但是要注意 \(k=0\) 不是无解的。
int num;
std::vector<int> ans;
int main() {
scanf("%lld", &k);
if (k == 0) {
printf("4\n3 4 1 2");
return 0;
}
if (k == 1) {
printf("6\n1 1 4 5 1 4");
return 0;
}
k --;
while (k) {
int cur = k & -k;
int m = log2(cur);
num ++;
for (int i = 1; i <= m; i ++)
ans.push_back(num);
ans.push_back(++ num);
k -= cur;
}
printf("%d\n", ans.size());
for (auto x : ans)
printf("%d ", x);
return 0;
}
G. Glass Bead Game 概率
现在有 \(n\) 个玻璃珠排成一排,给你 \(n\) 个概率 \(p\),其中 \(\sum_{i=1}^{n} p_i=1\),表示每次操作中,第 \(i\) 个玻璃珠有 \(p_i\) 的概率被移动到这一排的最左侧,同时这一次移动花费代价为这个玻璃珠到第 \(1\) 位的距离(如果本身就在最左侧则为 \(0\))。问经过无限多次移动后,再移动一次时的期望花费。\(n\leq 100\)。
一开始以为是按 \(p\) 递减排序后暴力几轮看期望,但是精度显然不够。还是要考虑求每个弹珠最后所在的期望位置。这个期望位置等于其余弹珠在他前面的概率之和。
设 \(p_{ji}\) 表示 \(j\) 弹珠在 \(i\) 弹珠前面的概率,则 \(p_{ij}+p_{ji}=1\),同时有以下关系式。
- 前一项是某一次移动前 \(j\) 在 \(i\) 前,则除了 \(p_i\) 的概率将 \(i\) 移动到前面否则 \(j\) 在 \(i\) 前可以维持;
- 后一项是某一次移动前 \(i\) 在 \(j\) 前,则只有 \(p_j\) 的概率将 \(j\) 移动到最前面才能 \(j\) 在 \(i\) 前。
继续推以下,得
于是第 \(i\) 个珠子的期望位置
最后总的期望为
int n;
double p[N];
int main(){
scanf("%d",&n);
for(int i=1;i<=n;++i)scanf("%lf",&p[i]);
sort(p+1,p+1+n,greater<double>());
double ans=0;
for(int i=1;i<=n;++i){
double dis=0;
for(int j=1;j<=n;++j){
if(i!=j)dis+=(p[j])/(p[i]+p[j]);
}
ans+=dis*p[i];
}
printf("%.10lf\n",ans);
return 0;
}
E. Easy String Problem 思维+莫队
给一个长为 \(n\) 的字符串,字符集大小也为 \(n\)。
现在有 \(q\) 个询问,每次询问给出一个区间 \([l,r]\),回答你能够得到多少个不同的字符串,这些字符串是由某一个包含 \([l,r]\) 的子串移除后再将剩余部分首尾拼接得到的。
\(3\leq n\leq 10^5,q\leq 10^5\)
包含 \([l,r]\) 的所有子串个数为 \(l\times (n-r+1)\),减去删去某一子串后重复的即可。
现在考虑求哪些字串删去后留下的字符串相同。观察(队友观察的)得到
如果 \(i\in[1,l]\) 和 \(j\in [r,n]\) 满足 \(s_i=s_j\),则删除 \([i,j-1]\) 和 \([i+1,j]\) 这两个子串后得到的字符串相同。进一步可以发现,我们重复统计的次数就是 \(i\in [i,l],j\in [r,n]\) 中满足 \(s_i=s_j\) 的点对数。这个问题可以使用莫队算法解决。
具体实现见代码,注意四个循环位置,具体见 OI-wiki。
#include <bits/stdc++.h>
using namespace std;
const int N=200005;
typedef long long ll;
struct quest{
int l,r,id;
}que[N];
int n,m,a[N],siz;
ll ans[N],l=1,r,now,bot[N][2];
bool cmp(quest x,quest y){
return (x.l/siz==y.l/siz)?(((x.l/siz)&1)?x.r<y.r:x.r>y.r):x.l<y.l;
}
int main(){
scanf("%d",&n);
l=1,r=n;
for(int i=1;i<=n;++i){
scanf("%d",&a[i]);
}
scanf("%d",&m);
siz=n/max(1,(int)sqrt((double)m*2/3));
for(int i=1;i<=m;++i){
scanf("%d%d",&que[i].l,&que[i].r);
que[i].id=i;
}
sort(que+1,que+1+m,cmp);
for(int i=1;i<=m;++i){
ll ql=que[i].l,qr=que[i].r;
while(l>ql){
now-=bot[a[--l]][1];
--bot[a[l]][0];
}
while(r<qr){
now-=bot[a[++r]][0];
--bot[a[r]][1];
}
while(r>qr){
now+=bot[a[r]][0];
++bot[a[r--]][1];
}
while(l<ql){
now+=bot[a[l]][1];
++bot[a[l++]][0];
}
ans[que[i].id]=1ll*que[i].l*(n-que[i].r+1)-now;
}
for(int i=1;i<=m;++i){
printf("%lld\n",ans[i]);
}
return 0;
}
C. Cup of Water 容斥+组合数学
现有一个容积为 \(1\) 的杯子,每次你可以在 \([0,x]\) 范围内给定随机实数 \(t\),向杯中倒入 \(t\) 的水。问将杯子倒满的期望次数。
\(T\leq 10000,0.05\leq x\leq 10^9\)
史诗期望题,强力安利我的详细题解。
L. Light of Stars 简单几何+数据结构
非负平面上有 \(n\) 个星星,第 \(i\) 个星星的非负整数坐标为 \((x_i,y_i)\),每个星星可以照亮相对自身的相同的 \(k\) 个区域,每个区域以一个角度 \([l_j,r_j]\) 表示,表示这个区域在 \(x\) 轴正方向分别顺时针旋转 \(l_j\degree\) 和 \(r_j\degree\) 的两个射线之间,保证每个星星照亮的区域互不重叠。现在问你每个星星被多少个其余的星星照亮。
\(n\leq 10^5,k\leq 10,0\leq x_i,y_i\leq 50000, 0\leq l_i\leq r_i<180\)
我们现在考虑某个星星 \((x_i,y_i])\) 的某个区域 \([l_j,r_j]\) 可以照亮哪些星星。这个问题可以转化为区域 \([l_i,180]\) 照亮的星星,减去 \((r_j,180]\) 照亮的星星,这两个问题可以用类似的方法处理(尽管开闭区间不同,但是我们可以转化为减去 \([r_j+\epsilon,180]\) 照亮的星星,\(\epsilon\) 即为程序中常使用的 EPS)。
将以 \((x_i,y_i)\) 和 \((x',y')\) 为端点,方向为 \(l_j\degree\) 的射线做出来,如果 \((x_i,y_i)\) 的射线在 \((x',y')\) 的射线
“上方“且 \(y_i\geq y'\),那么 \((x',y')\) 就可以被照亮。
转化为数学语言,可用通过这两条射线与 \(x\) 轴的交点描述,即 \(x'+y'\times \csc\frac{l_j\pi}{180}\leq x_i+y_i\times \csc \frac{l_j\pi}{180}\)。
这样将每个点先按纵坐标从大到小排序,在按 \(v_i=x_i+y_i\times \csc\frac{l_j\pi}{180}\) 从大到小排序,看比当前点 \(v_i\) 大的点的个数即是被照亮的次数。这个东西可以使用树状数组来做。
最后对一共 \(k\leq 10\) 个区间都做一次,每个区间包含一次加一次减即可。
然后注意 \(l_i=0\) 的细节问题即可,可以直接特判一下,具体实现见代码。
#include <bits/stdc++.h>
using namespace std;
const int N=100005;
const double pi=acos(-1);
const double EPS=1e-10;
int n,k,ans[N],x[N],y[N];
int c[N];
inline int lowbit(int x){return x&-x;}
void update(int i,int v){
for(;i<=n;i+=lowbit(i))
c[i]+=v;
}
int getsum(int i,int v=0){
for(;i;i-=lowbit(i))
v+=c[i];
return v;
}
struct node{
int id,x,y;
double z;
}a[N];
double lis[N];
inline bool cmp(node x,node y){
if(x.y==y.y)return x.z>y.z;
return x.y>y.y;
}
int bot[N];
void solve0(int f){
for(int i=0;i<N;++i)bot[i]=0;
for(int i=1;i<=n;++i){
a[i].id=i;
a[i].x=x[i],a[i].y=y[i];
}
for(int i=1;i<=n;++i){
++bot[a[i].y];
}
for(int i=1;i<N;++i){
bot[i]+=bot[i-1];
}
for(int i=1;i<=n;++i){
ans[i]+=(a[i].y>0?bot[N-1]-bot[a[i].y-1]:bot[N-1])-1;
}
}
void solve(int deg,int f){
if(deg==0&&f==1){
solve0(f);
return;
}
for(int i=0;i<=n;++i)c[i]=0;
double ang=1.0*deg/180.0*pi;
for(int i=1;i<=n;++i){
double tmp=0;
if(deg!=0)tmp=x[i]+y[i]/tan(ang);
else tmp=y[i]*N+x[i];
a[i].id=i;
a[i].x=x[i],a[i].y=y[i];
lis[i]=a[i].z=tmp;
}
sort(lis+1,lis+1+n,less<double>());
sort(a+1,a+1+n,cmp);
for(int i=1;i<=n;++i){
int loc=lower_bound(lis+1,lis+1+n,a[i].z)-lis;
int pre=lower_bound(lis+1,lis+1+n,a[i].z-f*EPS)-lis;
ans[a[i].id]+=f*(getsum(n)-getsum(pre-1));
update(loc,1);
}
}
int main(){
scanf("%d%d",&n,&k);
for(int i=1;i<=n;++i){
scanf("%d%d",&x[i],&y[i]);
}
for(int i=1;i<=k;++i){
int lef,rit;
scanf("%d%d",&lef,&rit);
solve(lef,+1);
solve(rit,-1);
}
for(int i=1;i<=n;++i){
printf("%d ",ans[i]);
}puts("");
return 0;
}
J. Just Another String Problem KMP
给一个长为 \(n\) 的字符串和一个长为 \(n\) 的不降权值数组 \({v_1,v_2,\dots,v_n}\)。
一个字符串组成的非空集合 \(T\) 是”好“的当且仅当满足:
- 不存在 \(T\) 中的一个字符串是另一个 \(T\) 中的字符串的后缀(含自身)。
对于一个好的非空集合 \(T\),其权值是 \(\sum_{t\in T}v_{|t|}\),其中 \(|t|\) 是字符串 \(t\) 的长度。
对于一个字符串 \(s\) 和一个正整数 \(k\),定义 \(f(s,k)\) 是权值最大的 \(T\) 的权值,\(T\) 满足:
- \(|T|\leq k\),即 \(T\) 中的字符串个数小于等于 \(k\)。
- \(T\) 中所有字符串均为 \(s\) 的子串(含 \(s\) 自身)。
现在给出 \(q\) 个询问 \(l,k\),要求 \(f(s_{1\sim l},k)\)。
\(1\leq n,q\leq 2\times 10^5,1\leq k\leq 10^9,l\leq n\)
\(\sum n,\sum q\leq 10^6\)
主要是题目长加上字符串,其实感觉不比前面的题尤其是C题难(虽然补题wa了两发qwq)。
首先 \(k>l\) 是没有意义的,因为任意两个 \(T\) 中的子串不能公用右端点,否则构成后缀关系,则 \(\max |T|\leq l\)。
其次由于 \(v\) 是一个不降的序列,要求我们取的字符串尽可能长,这就给出一个结论,假设取了子串 \(s_{l_i\sim r_i}\),不如直接取 \(s_{1\sim r_i}\),因为这样子并不会构成新的后缀关系而导致冲突。所以只会取 \(s\) 的前缀。
进一步,如果取了某个前缀 \(s_{1\sim i}\),则不能取这个前缀的任意一个 border。于是要做 KMP,定义前缀 \(s_{1\sim i}\) 的极长 border 为 \(s_{1\sim nxt(i)}\)。
由于要最优,如果我们取了 \(s_{1\sim nxt(i)}\),不如直接取 \(s_{1\sim i}\),于是我们答案取的前缀在 \(s_{1\sim l}\) 中应当不是任意一个前缀的 border。
根据 border 的性质,假如 \(s_{1\sim nxt(i)}\) 是 \(s_{1\sim i}\) 的 border,则 \(s_{1\sim nxt(i)-1}\) 也一定是 \(s_{1\sim i-1}\) 的一个 border,故如果我们不能取 \(s_{1\sim nxt(i)}\) 作为答案,则也不能取 \(s_{1\sim nxt(i)-1}\) 作为答案。
于是我们求 \(pre=\max_{i=1}^{l}nxt(i)\),则对于所有长度大于 \(pre\) 的前缀,他们都不会被作为 border,而对于长度小于等于 \(pre\) 的前缀他们都可以表示成一个 border,于是答案最大即为
时间复杂度 \(O(n)\),比官方题解要好(其实我没看懂官方题解)。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int>ttfa;
const int N=500005;
const ll INF=0x3f3f3f3f3f3f3f3f;
int n,m;
char s[N];
ll val[N],sum[N];
int nxt[N],pre[N];
int mainT(){
scanf("%d%d",&n,&m);
scanf("%s",s+1);
for(int i=1;i<=n;++i){
scanf("%lld",&val[i]);
sum[i]=sum[i-1]+val[i];
}
for(int i=2,j=0;i<=n;++i){
while(j&&s[i]!=s[j+1])j=nxt[j];
if(s[i]==s[j+1])++j;
nxt[i]=j;
}
for(int i=1;i<=n;++i){
pre[i]=max(pre[i-1],nxt[i]);
}
for(int T=1;T<=m;++T){
int x,k;scanf("%d%d",&x,&k);
ll ans=sum[x]-sum[max(x-k,pre[x])];
printf("%lld\n",ans);
}
return 0;
}
int main(){
int Test;scanf("%d",&Test);
while(Test--){
mainT();
}
return 0;
}

浙公网安备 33010602011771号