300iq contest系列做题记录

缓慢更新中.jpg

J. Jealous Split

想不到的转化方式系列(

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

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

下面证明一下这个结论

考虑一个划分点$k$

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

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

设这段和为$s$

条件即转化为:$(s_1+s)^2 + (s_2-s)^2 <{s_1}^2 + {s_2}^2$

整理一下

$2*s(s1-s2)+2s^2<0$

$s1-s2+s<0$

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

于是有$(s_1-s_2)^2$ < $t^2$($t$是$max$)

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

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

左侧=$|s_1+s-(s_2-s)|=|s_1 - s_2 +2s|$

左侧的平方:

$(s_1 - s_2)^2 +4s^2+4s*(s_1 - s_2)$<$t^2$+$4s*(s+s_1 -s_2)$<$t^2$

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

决策点左移类似的证即可

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

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

 

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

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

可以发现的是,要让$|s_1 - s_2|$尽可能地小

也就是说$|s_1|$ 和 $|s_2|$要尽可能地靠近

又有$|s_1| + |s_2|$是个定值

$s_1^{2} + s_2^{2} +2*(s_1*s_2)$是个定值

这时候要最小化$s_1^{2}+s_2^{2}-2*(s_1*s_2)$ 也就是$t-4*(s_1*s_2)$

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

$s_1*s_2$认为是左侧的乘上右侧的,其实这个值就会是$sum^2$ - (左侧单独两两乘法) - (右侧单独两两乘法)

而$sum^2$固定,也就是让左侧单独两两乘法,右侧单独两两乘法的和最大。

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

于是就转化到原结论上了

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

#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$条边,选权重最大的一堆无环边,以$y_i$做一个线性基

考虑无环边怎么处理。

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

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

对于第二条边同理

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

于是也无环也变成了任意异或和不为$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

和$19-20$澳门那道$I$题的做法几乎一模一样。

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

对于每个连通块开一个堆,堆内部是$\frac{c}{2}$的大小

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

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

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

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

瞎总结一下(

如果遇到这类,有限个数字相加(并且数量不会很大)的时候,可以考虑设置一个阈值,在每次碰到阈值的时候再拉出来检定,如果能保证碰到阈值的次数(对于这题来说是$\frac{c}{2}$,对于那题澳门是$\frac{n}{k}$)是可以接受的范围内(一般可以证明到这玩意应该增长是指数级的,所以是一个$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$个,也就是一个组合数的后缀和。

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

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

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

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

考虑$|S|<=N(S)$的集数,这个对数显然是$2^N - k -1$

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

从下往上贪心即可。

 

瞎总结:

这种构造一堆元素,然后合出一个恰有$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$个奇数,凑出$2N-x-1$

转化一下,用$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 @ 2022-03-25 00:48  si_nian  阅读(103)  评论(0)    收藏  举报