USACO 2025 一月月赛题解
我知道你很急。可是比赛还没结束。所以你先别急。
下文更新于 1 月 28 日 21:55。此时比赛已经结束。
T1
回忆版题意:给一张
做法:假装最开始一条边都没有,你都要加入,有的边要付出正代价,其它要付出负代价,最后答案加上已有的所有边权之和。接下来做法就呼之欲出了。显然每棵子树对应的点集是一个区间,因此可以考虑 区间 dp。
设
不难发现,转移可以通过移除
因为是 dfs 树,所以只能有返祖边和树边,并且树边一定要被选,因此这个方案的贡献还要加上
代码:
#include<bits/stdc++.h>
using namespace std;
int a[755][755];
int f[755][755];
int main(){
int n;
scanf("%d",&n);
int he=0;
for(int i=1;i<=n;++i)for(int j=1;j<i;++j){
scanf("%d",&a[j][i]);
if(a[j][i]<0)he+=-a[j][i];
}
memset(f,63,sizeof(f));
for(int i=n;i>=1;--i){
f[i][i]=0;
for(int j=i;j<=n;++j){
int he=max(0,a[i][j+1]);
for(int k=j+1;k<=n;++k){
he+=min(0,a[i][k]);
f[i][k]=min(f[i][k],f[i][j]+f[j+1][k]+he);
}
}
}
printf("%d\n",he+f[1][n]);
return 0;
}
T2
回忆版题意:给你一个长度为
做法:先不说赛时想法了,太唐了,沿着线性规划想了很久居然没找出错(这是因为这个操作数量不是整数或许能更优!),然后从矩阵逆的角度想了很久(以为这是个考虑
观察:两边的位置记为
其实是可以的。注意到
则序列如果满足以下条件,我们就可以得到一个
看起来是个半平面交状物。但是因为只是要最小化
因为
模仿联合省选季风的方法,可以算出
如果暴力枚举
考虑优化。第一点,中途一个
第二点也就是最后的优化,枚举每个
让我们回顾一下
问题转化成非常多个限制
我们又不难看出,因为上式分子变化量只有
最后总复杂度是什么呢?因为最开始二分要
代码(目前能过拍,不确定正确性和常数,实现难度适中):
#include<bits/stdc++.h>
using namespace std;
int n;
long long p[100005];
bool pan(long long h){
if(h<0)return 0;
__int128 L=0,R=h;
for(int i=0;i<=n;++i){
__int128 c=p[i]-(__int128)h*(n-i);
if(2*i==n){
if(0<c)return 0;
continue;
}
if(2*i<n){
if(c>0)return 0;
R=min(R,(-c)/(n-2*i));
}else{
L=max(L,(c+2*i-n-1)/(2*i-n));
}
}
return L<=R;
}
vector<pair<int,int>>vc;
__int128 xqz(__int128 a,int b){
return (a-(a%b+b)%b)/b;
}
void fen(__int128 z,int x,int m){
for(int i=x;i<=n;){
int j=xqz(z+i-x,m)*m+m-1+x-z;j=min(j,n);
vc.emplace_back(i,j);
i=j+1;
}
for(int i=x-1;i>=0;){
int j=z+x-xqz(z+x-i,m)*m-m+1;j=max(j,0);
vc.emplace_back(j,i);
i=j-1;
}
}
void nef(__int128 z,int x,int m){
for(int i=x;i<=n;){
int j=x+z-xqz(z-i+x,m)*m;j=min(j,n);
vc.emplace_back(i,j);
i=j+1;
}
for(int i=x-1;i>=0;){
int j=xqz(z-x+i,m)*m-z+x;j=max(j,0);
vc.emplace_back(j,i);
i=j-1;
}
}
struct maot{
long long b[17][100005],a[100005];
int lg[100005];
void preinit(){
lg[1]=0;
for(int i=2;i<=n+1;++i)lg[i]=lg[i>>1]+1;
}
void init(long long x){
for(int i=0;i<=16;++i)for(int j=0;j<=n;++j)b[i][j]=x;
}
inline void gai(int l,int r,long long x){
int cd=lg[r-l+1];
b[cd][l]=min(b[cd][l],x);
b[cd][r-(1<<cd)+1]=min(b[cd][r-(1<<cd)+1],x);
}
void solve(){
for(int i=16;i>=1;--i)for(int j=0;j<=n-(1<<i)+1;++j){
b[i-1][j]=min(b[i-1][j],b[i][j]);
b[i-1][j+(1<<(i-1))]=min(b[i-1][j+(1<<(i-1))],b[i][j]);
}
for(int i=0;i<=n;++i)a[i]=b[0][i];
}
}gl,gr;
bool ok(__int128 h){
if(h<=0)return 0;
--h;gl.init(-1);gr.init(1+h);
for(int i=0;2*i<n;++i){
__int128 c=h*(n-i)-p[i]+n-2*i;
vc.clear();fen(c,i,n-2*i);
for(auto pi:vc){
int j=pi.first,k=pi.second;
__int128 p=(c+abs(i-j))/(n-2*i);
if(p<=h){
gr.gai(j,k,p);
}
}
}
for(int i=n;2*i>n;--i){
__int128 c=p[i]-h*(n-i)+4*i-2*n-1;
vc.clear();nef(c,i,2*i-n);
for(auto pi:vc){
int j=pi.first,k=pi.second;
__int128 p=(c-abs(i-j))/(2*i-n);
if(p>1){
gl.gai(j,k,-p);
}
}
}
gl.solve();gr.solve();
for(int i=0;i<=n;++i){
if(n%2==0&&p[n/2]-abs(i-n/2)>h*n/2)continue;
if(gl.a[i]+gr.a[i]==0)return 1;
}
return 0;
}
int main(){
int T;
scanf("%d",&T);
while(T--){
scanf("%d",&n);
--n;
for(int i=0;i<=n;++i)scanf("%lld",&p[i]);
int fl=1;
for(int i=0;i<=n;++i)fl&=(p[i]==0);
if(fl){
puts("0");
continue;
}
gl.preinit();gr.preinit();
long long L=0,R=2e18;
while(L<=R){
long long h=(L+R)>>1;
if(pan(h))R=h-1;
else L=h+1;
}
if(ok(L-1))--L;
printf("%lld\n",L);
}
return 0;
}
T3
回忆版题意:给定两个正整数序列
做法:首先观察到即使我将操作加强到可以操作任意非负实数次,所需的代价显然不会变,你可以很容易通过一些极端原理来说明。因此可以考虑线性规划(?),将问题对偶为下面的问题,并不改变难度(完全不理解我为什么要干这个对偶,本质完全相同啊,笑喷):
给
并要求最大化
看着很吓人的问题,不过先打个暴力 dp 吧。设
不妨认可这个性质是对的。接下来你会做了吗?
无所谓,我们一步一步来!先写出暴力 dp(注意第二维的范围应该限制在
假如序列有上凸性(即差分单调不增),则我们可以通过 slope trick 的方式只维护斜率,并且可以重新复述下 dp 流程:
- 把
f_{i-1} 序列改为其前缀最大值。 - 将
f_{i-1} 的定义域限制在[0,c_i] 内。 - 将
f_{i-1} 序列翻转得到目前暂时的f_i 。 - 对所有
j ,将f_{i,j} 加上w_ij 。 - 求出答案,也就是
f_i 的前缀最大值。
接下来对应到差分序列上(注意它单调不增,且可以把相邻相等的捏到一起):
- 删掉后缀所有
<0 的数,把不足的补0 。 - 如果目前总长超过了限制,删掉一段后缀,否则补
0 。 - 翻转并取反。
- 整体加
w_i 。 - 同时记录下
f_{i,0} 点处的值,则答案为这个值加上序列中所有>0 的数的和。
接下来我们考虑维护标记:
考虑如何使这些标记封闭:
- 加一个数
x 等价于在序列中添加一个A(x-B) ,注意这里A\in \{1,-1\} 。 - 翻转对应
fl 取反。值取反等价于将A,B 取反。此时f0 要加上he ,且he 也要取反。 - 整体加等价于
B 加。 - 最后一个小问题,求答案的时候,难不成还要维护前缀和吗?其实不用,这是因为,这些
\leq 0 的数在考虑下一个位置时的第一个操作中,也会被弹掉了。因此可以和下一次第一个操作放一起做。
复杂度均摊线性,看我们多么厉害!居然可以
此外,这个题目也表明如下一个观点:遇到类似线性规划的问题可能会有很多种办法,急着对偶未必是一个好的选择!
代码:
#include<bits/stdc++.h>
using namespace std;
int w[500005],c[500005];
deque<pair<int,long long>>dq;
long long A=1,B=0;
int fl=0;
long long h1,he;
long long f0;
void pback(int x,long long y){
h1+=x;he+=x*y;
if(fl==0)dq.emplace_back(x,(y-B)/A);
else dq.emplace_front(x,(y-B)/A);
}
void zhej(long long y){
he+=y*h1;B+=y;
}
void rev(){
fl^=1;f0+=he;
A*=-1,B*=-1;he=-he;
}
void qian(){
while(dq.size()){
if(fl==0){
if(A*dq.back().second+B<=0){
auto pi=dq.back();dq.pop_back();
he-=pi.first*(A*pi.second+B);
h1-=pi.first;
}else break;
}else{
if(A*dq.front().second+B<=0){
auto pi=dq.front();dq.pop_front();
he-=pi.first*(A*pi.second+B);
h1-=pi.first;
}else break;
}
}
}
void resz(int m){
if(h1<m){
pback(m-h1,0);
}else while(h1>m){
pair<int,long long>pi;
if(fl==0)pi=dq.back(),dq.pop_back();
else pi=dq.front(),dq.pop_front();
he-=pi.first*(A*pi.second+B);
h1-=pi.first;
if(h1>=m)continue;
pback(m-h1,A*pi.second+B);
}
}
void suanqian(){
while(dq.size()){
if(fl==0){
if(A*dq.back().second+B<=0){
auto pi=dq.back();dq.pop_back();
he-=pi.first*(A*pi.second+B);
h1-=pi.first;
}else break;
}else{
if(A*dq.front().second+B<=0){
auto pi=dq.front();dq.pop_front();
he-=pi.first*(A*pi.second+B);
h1-=pi.first;
}else break;
}
}
printf("%lld\n",f0+he);
}
void output(){
printf("!!! %lld\n",f0);
printf("? %d %lld %lld\n",fl,A,B);
printf("%lld %lld\n",h1,he);
int gs=0;long long g2=0;
for(auto pi:dq){
int T=pi.first;
gs+=pi.first;
while(T--)printf("%lld ",A*pi.second+B);
g2+=pi.first*(A*pi.second+B);
}
cout<<gs<<" "<<g2<<endl;
}
int main(){
int n;
scanf("%d",&n);
for(int i=1;i<=n;++i)scanf("%d",&w[i]);
for(int i=2;i<=n;++i)scanf("%d",&c[i]);
f0=0;pback(c[2],w[1]);qian();
for(int i=2;i<=n;++i){
resz(c[i]);
rev();
zhej(w[i]);
suanqian();
}
// output();
return 0;
}
本文完稿于 1 月 28 日 23:02 分。