300iq contest系列做题记录
缓慢更新中.jpg
J. Jealous Split
想不到的转化方式系列(
最优的划分方案一定是和的平方的和最小的子段划分方案
这东西直接二分+斜率优化解决就行了
下面证明一下这个结论
考虑一个划分点
不妨设将右移到之后,平方和会变小
也就是说,对于左侧来说,它增加了从到这一部分的和,而右边减小了这个和
设这段和为
条件即转化为:
整理一下
然后我们再假设,第一种划分是合法的
于是有 < (是)
接下来我们要证明第二种划分也是合法的
对于第二种划分方案来说:
左侧=
左侧的平方:
<+<
也就是说新的这个划分也一定是合法的划分
决策点左移类似的证即可
也就是说我们证明了,如果让平方和更小,一定不会令答案更劣
于是也就是说,找平方和最小的答案即可。
瞎yy一下这玩意怎么想的(
考虑两个端点固定的进行一次划分
可以发现的是,要让尽可能地小
也就是说 和 要尽可能地靠近
又有是个定值
是个定值
这时候要最小化 也就是
也就是说,这个划分要让两边的乘积尽可能大
认为是左侧的乘上右侧的,其实这个值就会是 - (左侧单独两两乘法) - (右侧单独两两乘法)
而固定,也就是让左侧单独两两乘法,右侧单独两两乘法的和最大。
这种两两乘法,常用套路就是考虑和的平方,然后就会考虑到让左侧和做个平方,右侧和做个平方,出现左侧单独两两乘法,右侧单独两两乘法的情况,因为要最大化这个东西,所以其实是要最小化左侧和的平方和右侧和的平方的和。
于是就转化到原结论上了
虽然有朝着这个方面想过但是变量太多了就寄了,呜呜
#include<bits/stdc++.h> using namespace std; inline void read(__int128 &X) { X = 0; int w=0; char ch=0; while(!isdigit(ch)) {w|=ch=='-';ch=getchar();} while(isdigit(ch)) X=(X<<3)+(X<<1)+(ch^48),ch=getchar(); if (w) X = -X; } void print(__int128 x) { if (!x) return ; if (x < 0) putchar('-'),x = -x; print(x / 10); putchar(x % 10 + '0'); } int N,M; int lst[100005]; __int128 ans,x[100005],Sum[100005],dp[100005],g[100005],mx[100005]; int que[100005]; __int128 Getx(int pos){ return Sum[pos]; } __int128 Gety(int pos){ return dp[pos]+Sum[pos]*Sum[pos]; } bool Check(__int128 mid){ //print(mid); //cout<<endl; for (int i=1;i<=N;i++) g[i]=mx[i]=0; int head=1,tail=1; que[1]=0; for (int i=1;i<=N;i++){ __int128 nn=2ll*Sum[i]; while (head<tail && nn*(Getx(que[head+1])-Getx(que[head])) > (Gety(que[head+1])-Gety(que[head]))) head++; dp[i]=dp[que[head]]+((Sum[i]-Sum[que[head]])*(Sum[i]-Sum[que[head]])+mid); g[i]=g[que[head]]+1; while (head<tail && (Gety(que[tail])-Gety(que[tail-1]))*(Getx(i)-Getx(que[tail-1])) > (Gety(i)-Gety(que[tail-1]))*(Getx(que[tail])-Getx(que[tail-1]))) tail--; que[++tail]=i; } if (g[N]<=M) return true; else return false; } int main(){ //freopen("data.in","r",stdin); //freopen("test1.out","w",stdout); scanf("%d%d",&N,&M); for (int i=1;i<=N;i++){ read(x[i]); Sum[i]=Sum[i-1]+x[i]; } __int128 l=0,r=1e20; while (l<=r){ __int128 mid=(l+r)>>1ll; if (Check(mid)) r=mid-1,ans=mid; else l=mid+1; } Check(ans); __int128 val=dp[N]-M*ans; vector<int> b; b.push_back(N); for (int i=N-1;i;i--){ __int128 nw=(Sum[b.back()]-Sum[i])*(Sum[b.back()]-Sum[i]); if (g[i]+1<=M && dp[i]-(M-1)*ans+nw==val) { b.push_back(i); --M; val-=nw; } } reverse(b.begin(),b.end()); printf("Yes\n"); for (auto xx:b) if (xx!=N) printf("%d ",xx); return 0; }
E-Easy win
也是一个转化题意好题
题意其实是,给你条边,选权重最大的一堆无环边,以做一个线性基
考虑无环边怎么处理。
考虑对于一条边和一条边
我们开一个路径二进制,对于第一条边来说,和都为
对于第二条边同理
我们考虑走过,之后,这个二进制数变为,也就是
于是也无环也变成了任意异或和不为问题,也去线性基就行了。
然后这题的线性基比较特殊,是动态求解的。
顺手记录一下每个基包括的数字即可。
#include <bits/stdc++.h> using namespace std; #define Bit bitset<128> int N,T; long long ans; int bs[50005],w[50005]; int cnt=0; Bit a[50005],b[50005]; void Add(Bit nw,long long val){ Bit d; d.reset(); for (int i=128;i>=0;i--) if (nw[i]){ if (!bs[i]){ bs[i]=1; ans+=val; w[++cnt]=val; d[cnt]=1; a[i]=nw,b[i]=d; return; } else{ nw^=a[i],d^=b[i]; } } int flag=-1; for (int i=1;i<=cnt;i++) if (d[i]) if (flag==-1 || w[i]<w[flag]) flag=i; if (val>w[flag]){ ans+=val-w[flag]; w[flag]=val; d[flag]=0; for (int i=0;i<=128;i++) if (bs[i] && b[i][flag]) b[i]^=d; } } int main(){ scanf("%d%d",&N,&T); for (int i=1;i<=T;i++){ int x,y; long long z; long long v; scanf("%d%d%lld%lld",&x,&y,&z,&v); x--,y--; Bit nw; nw.reset(); nw[x]=1,nw[y]=1; for (int j=0;j<=60;j++){ if ((z>>j)&1) nw[64+j]=1; else nw[64+j]=0; } Add(nw,v); printf("%lld\n",ans); } return 0; }
F. Fast Spanning Tree
和澳门那道题的做法几乎一模一样。
考虑这种的形式,一定是有一个数大于,于是我们对于每个限制记录它的
对于每个连通块开一个堆,堆内部是的大小
只有在当前连通块的点数大于它的堆顶点数的时候,把这条边拉出来检定,如果它合法了,就把它加进去
如果它不合法,那么就先把已有的扣掉后重新分配到两边的点所在的连通块内就行
这么做的话, 每次数字下降是,所以是一个的
具体证明也可以看上次写的那个题的做法,这个题比那个题好写多了(
瞎总结一下(
如果遇到这类,有限个数字相加(并且数量不会很大)的时候,可以考虑设置一个阈值,在每次碰到阈值的时候再拉出来检定,如果能保证碰到阈值的次数(对于这题来说是,对于那题澳门是)是可以接受的范围内(一般可以证明到这玩意应该增长是指数级的,所以是一个),就可以采用不断暴力取出再重设阈值放回堆内的方法。
#include <bits/stdc++.h> using namespace std; int N,M,fa[300005]; int val[300005]; const int mx=1e6; vector<int> ans; priority_queue<pair<int,int> > nw[300005]; priority_queue<int> Can; int Getfa(int x){ return (fa[x]==x)?x:(fa[x]=Getfa(fa[x]));} struct Node{ int x,y,s; }a[300005]; void Add(int x){ int fx=Getfa(a[x].x),fy=Getfa(a[x].y); if (fx==fy) return; if (val[fx]+val[fy]>=a[x].s){ Can.push(-x); return; } int res=a[x].s-val[fx]-val[fy]+1; nw[fx].push({-(val[fx]+res/2),-x}); nw[fy].push({-(val[fy]+res/2),-x}); } void Merge(int x,int y,int pos){ int fx=Getfa(x),fy=Getfa(y); if (fx==fy) return; ans.push_back(pos); if (nw[fx].size()>nw[fy].size()) swap(fx,fy); fa[fx]=fy;val[fy]+=val[fx]; val[fy]=min(val[fy],mx); while (!nw[fx].empty()){ pair<int,int> nww=nw[fx].top();nw[fx].pop(); int d=-nww.first; if (val[fy]>=d) Add(-nww.second); else nw[fy].push(nww); } while (!nw[fy].empty()){ pair<int,int> nww=nw[fy].top(); nw[fy].pop(); int d=-nww.first; if (val[fy]>=d) Add(-nww.second); else { nw[fy].push(nww); return; } } } int main(){ scanf("%d%d",&N,&M); for (int i=1;i<=N;i++){ scanf("%d",&val[i]); } for (int i=1;i<=N;i++) fa[i]=i; for (int i=1;i<=M;i++){ scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].s); Add(i); } while (!(Can.empty())){ int x=Can.top(); x=-x; Can.pop(); Merge(a[x].x,a[x].y,x); } printf("%d\n",ans.size()); for (auto x:ans){ printf("%d ",x); } return 0; }
H. Hall's Theorem
感觉构造题做的比以前顺手多了(
题意大概是给二分图的两列点数量,左向右连边,构造满足的左侧点集的数量恰为的图,其中是右侧与左侧关联点的数量。
首先,这种构造恰有个的题,一般是一堆性质相同的东西随便选出一部分,然后构造性质不同的部分相互独立(不太知道怎么描述这个事情,大概意思就是一堆式子加加减减凑出一个)
于是就会想到先考虑,向一些相同的点连边,不妨设相同的点点数为,这样的点总共有个
于是就会发现,要满足,其实就是从里面选择至少个,至多个,也就是一个组合数的后缀和。
然后考虑,如果有一些点向着这个相同点集中的相同的一部分再连边(换言之,连的是一个当前选择的集合的一个子集),那么显然,这些点可以被加入统计,同时那些连接子集的点可以加入集合,设总共有个这样的点,它们连向至少个点
不难发现,一定是大于的,且又有是小于的,类似的,这个新的问题的求解也会是一个组合数的后缀和。
于是问题就转化成了找一系列组合数的后缀和,让它凑出来,且组合数需要满足:从下往上,后缀和的左端点依次递增(准确的说是不减)。
这个问题看着不太舒服,于是我们不妨反过来考虑一下
考虑的集数,这个对数显然是
于是就可以发现,这时候变成了找一系列前缀和,且右端点依次递增。
从下往上贪心即可。
瞎总结:
这种构造一堆元素,然后合出一个恰有个满足题目条件的东西的集合,经常是考虑把原本的元素划分成一些具有相似性质的集,集内的计算一般会是一个组合数或者乘法原理;然后再尽量让不同集之间独立或者影响较小,最终搞出一个合法的解。
#include <bits/stdc++.h> using namespace std; int C[55][55]; void Pre(){ for (int i=0;i<55;i++){ C[i][0]=1;C[i][i]=1; for (int j=1;j<i;j++){ C[i][j]=C[i-1][j]+C[i-1][j-1]; } } } int main(){ int N,K; scanf("%d%d",&N,&K); K=(1<<N)-K-1; Pre(); vector<pair<int,int> > as; int nww=0; for (int i=N;i>=1;i--){ for (int j=1;j<=N;j++){ int x,y; x=i-1,y=j-1; if (nww+C[x][y]>K){ break; } else { nww+=C[x][y]; as.push_back({i,j}); } } } printf("%d\n",as.size()); for(auto x:as) printf("%d %d\n",x.first,x.second); return 0; }
G.Graph Counting
题解告诉我们结论的图的充要条件是个点连接所有点,删掉个点后形成个奇团
至于这个证明的话,考虑定理的过程,连接一条边可以删掉两个奇团。
然后问题变成枚举,用个奇数,凑出
转化一下,用个任意整数凑出
最后问题是问,数字个数不为的拆分数
直接五边形数。
#include <bits/stdc++.h> using namespace std; const int fish=998244353; int f[500005],n,m,g[500005]; int N; int main(){ scanf("%d",&N); N++; f[0]=f[1]=1; for (int i=1;i*(3*i-1)/2<=N;i++) g[m++]=1ll*i*(3ll*i-1)/2,g[m++]=1ll*i*(3ll*i+1ll)/2; for (int i=2;i<=N;i++) for (int j=0;j<m&&g[j]<=i;j++){ f[i]=(f[i]+(((j>>1&1)?-1ll:1ll)*f[i-g[j]])); f[i]=f[i]<0?f[i]+fish:f[i]; f[i]=f[i]>fish?f[i]-fish:f[i]; } printf("%d\n",(f[N]-1ll+fish)%fish); return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?