300iq contest系列做题记录

缓慢更新中.jpg

J. Jealous Split

想不到的转化方式系列(

最优的划分方案一定是和的平方的和最小的子段划分方案

这东西直接wqs二分+斜率优化解决就行了

下面证明一下这个结论

考虑一个划分点k

不妨设将k右移到k1之后,平方和会变小

也就是说,对于左侧来说,它增加了从kk1这一部分的和,而右边减小了这个和

设这段和为s

条件即转化为:(s1+s)2+(s2s)2<s12+s22

整理一下

2s(s1s2)+2s2<0

s1s2+s<0

然后我们再假设,第一种划分是合法的

于是有(s1s2)2 < t2(tmax)

接下来我们要证明第二种划分也是合法的

对于第二种划分方案来说:

左侧=|s1+s(s2s)|=|s1s2+2s|

左侧的平方:

(s1s2)2+4s2+4s(s1s2)<t2+4s(s+s1s2)<t2

也就是说新的这个划分也一定是合法的划分

决策点左移类似的证即可

也就是说我们证明了,如果让平方和更小,一定不会令答案更劣

于是也就是说,找平方和最小的答案即可。

 

瞎yy一下这玩意怎么想的(

考虑两个端点固定的进行一次划分

可以发现的是,要让|s1s2|尽可能地小

也就是说|s1||s2|要尽可能地靠近

又有|s1|+|s2|是个定值

s12+s22+2(s1s2)是个定值

这时候要最小化s12+s222(s1s2) 也就是t4(s1s2)

也就是说,这个划分要让两边的乘积尽可能大

s1s2认为是左侧的乘上右侧的,其实这个值就会是sum2 - (左侧单独两两乘法) - (右侧单独两两乘法)

sum2固定,也就是让左侧单独两两乘法,右侧单独两两乘法的和最大。

这种两两乘法,常用套路就是考虑和的平方,然后就会考虑到让左侧和做个平方,右侧和做个平方,出现左侧单独两两乘法,右侧单独两两乘法的情况,因为要最大化这个东西,所以其实是要最小化左侧和的平方和右侧和的平方的和。

于是就转化到原结论上了

虽然有朝着这个方面想过但是变量太多了就寄了,呜呜

复制代码
#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

也是一个转化题意好题

题意其实是,给你n条边,选权重最大的一堆无环边,以yi做一个线性基

考虑无环边怎么处理。

考虑对于一条边(x,y)和一条边(y,z)

我们开一个路径二进制,对于第一条边来说,posxposy都为1

对于第二条边同理

我们考虑走过x>y,y>z之后,这个二进制数变为x>z,也就是axorb

于是也无环也变成了任意异或和不为0问题,也去线性基就行了。

然后这题的线性基比较特殊,是动态求解的。

顺手记录一下每个基包括的数字即可。

复制代码
#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

1920澳门那道I题的做法几乎一模一样。

考虑这种a+b>c的形式,一定是有一个数大于c2,于是我们对于每个限制记录它的c2

对于每个连通块开一个堆,堆内部是c2的大小

只有在当前连通块的点数大于它的堆顶点数的时候,把这条边拉出来检定,如果它合法了,就把它加进去

如果它不合法,那么就先把已有的扣掉后重新分配到两边的点所在的连通块内就行

这么做的话, 每次数字下降是/2,所以是一个log

具体证明也可以看上次写的那个I题的做法,这个题比那个题好写多了(

瞎总结一下(

如果遇到这类,有限个数字相加(并且数量不会很大)的时候,可以考虑设置一个阈值,在每次碰到阈值的时候再拉出来检定,如果能保证碰到阈值的次数(对于这题来说是c2,对于那题澳门是nk)是可以接受的范围内(一般可以证明到这玩意应该增长是指数级的,所以是一个log),就可以采用不断暴力取出再重设阈值放回堆内的方法。

复制代码
#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

感觉构造题做的比以前顺手多了(

题意大概是给二分图的两列点数量,左向右连边,构造满足|S|>N(S)的左侧点集的数量恰为k的图,其中N(S)是右侧与左侧关联点的数量。

 

首先,这种构造恰有k个的题,一般是一堆性质相同的东西随便选出一部分,然后构造性质不同的部分相互独立(不太知道怎么描述这个事情,大概意思就是一堆式子加加减减凑出一个k)

于是就会想到先考虑,向一些相同的点连边,不妨设相同的点点数为N(S)=K,这样的点总共有M

于是就会发现,要满足|S|>N(S),其实就是从M里面选择至少K个,至多M个,也就是一个组合数的后缀和。

然后考虑,如果有一些点向着这个相同点集中的相同的一部分再连边(换言之,连的是一个当前选择的集合的一个子集),那么显然,这些点可以被加入统计,同时那些连接子集的点可以加入集合,设总共有M1个这样的点,它们连向至少K1个点

不难发现,M1一定是大于M的,且又有K1是小于K的,类似的,这个新的问题的求解也会是一个组合数的后缀和。

于是问题就转化成了找一系列组合数的后缀和,让它凑出K来,且组合数需要满足:从下往上,后缀和的左端点依次递增(准确的说是不减)。

这个问题看着不太舒服,于是我们不妨反过来考虑一下

考虑|S|<=N(S)的集数,这个对数显然是2Nk1

于是就可以发现,这时候变成了找一系列前缀和,且右端点依次递增。

从下往上贪心即可。

 

瞎总结:

这种构造一堆元素,然后合出一个恰有n个满足题目条件的东西的集合,经常是考虑把原本的元素划分成一些具有相似性质的集,集内的计算一般会是一个组合数或者乘法原理;然后再尽量让不同集之间独立或者影响较小,最终搞出一个合法的解。

复制代码
#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


题解告诉我们结论的图的充要条件是x个点连接所有点,删掉x个点后形成x+2个奇团

至于这个证明的话,考虑tutte定理的过程,连接一条边可以删掉两个奇团。

然后问题变成枚举x,用x+2个奇数,凑出2Nx1

转化一下,用x个任意整数凑出N+1

最后问题是问N+1,数字个数不为1的拆分数

直接五边形数。

复制代码
#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;
} 
复制代码

 

posted @   si_nian  阅读(76)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
欢迎阅读『300iq contest系列做题记录』
点击右上角即可分享
微信分享提示