首页 > 其他分享 >wzOI 2023.8.24 模拟赛(附树的直径和三分法)

wzOI 2023.8.24 模拟赛(附树的直径和三分法)

时间:2023-09-08 10:56:38浏览次数:52  
标签:24 三分法 return int ll mid dfs 答案 wzOI

2023-08-28 15:53:53

$$A$$

典型错误

一知半解看样例型:

  • 如果该队某个数组的值比最大值大就算入答案。

上第一次厕所前我的思路:开局 \(30\) 分钟。

显然,我并不需要有一个数值最大才能赢,我只需要其中一个数值比 其中一个数值比 其中一个数值最大的那个 要大的队 要大即有可能获胜。(可以通过断句来理解)

看懂了但是没有完全理解,且没有深度分析大样例型:

  • 一个球队必输:当且仅当存在一个队伍,其每个属性都比它高。

上了第一次厕所后:开局第 \(30 \sim 100\) 分钟我一直把这个当作真理去打,就去找统计被淘汰的队伍数,结果发现大样例完全过不了。这时才分析大样例的 \(i=8\) 才发现,就算我所有数值都比一个组高,那个组还是可以通过打败可能打败我的组来获得胜利,因此这个策略错误。

正解

我苦思冥想,又上了一次厕所,然后在机房里散步(被吐槽了),在桌子上坐着。

然后想到了并查集合并。

发现了可以把每一个数值的上下界看作线段维护之后,很【容易想到用并查集维护 \(k\) 条线段构成的一个二维区间。我们用并查集维护每一个组的数量(顺便写个按秩合并),然后想什么情况合并——很简单,只要二维区间有交就合并。最后答案就是任意一个属性最大的那个区间(为什么?因为不会有交,所以每个集合之间相互独立,不可能出现一个值比一个组大,另外一个小于的情况,否则早被合并了)。

可能不太好理解,结合代码理解一下?

其实这道题跟并查集没有任何关系,只是我想用并查集写而已,关键在合并操作,而不是并查集。复杂度本来感觉过不了的啊,应该是没有把我卡掉的数据吧,大样例放洛谷上跑了 \(200ms\) 左右吧?

\(Code\)

#include<bits/stdc++.h>
using namespace std;
int read(){
    int x=0;char c=getchar();
    for(;!isdigit(c);c=getchar());
    for(;isdigit(c);c=getchar())x=(x<<3)+(x<<1)+(c^48);
    return x;
}
struct line{
    int u,d;
}a[6][100005];
int n,m,fa[100005],siz[100005];
unordered_set<int>S;
inline int find(int x){
    if(x==fa[x])return x;
    return fa[x]=find(fa[x]);
}
int merge(int x,int y){//合并操作
    if((x=find(x))==(y=find(y)))return y;
    if(siz[x]>siz[y])swap(x,y);
    for(int i=1;i<=m;i++){
        a[i][y].u=max(a[i][y].u,a[i][x].u);
        a[i][y].d=min(a[i][y].d,a[i][x].d);
    }
    fa[x]=y,siz[y]+=siz[x],siz[x]=0;
    return y;
}
bool check(int x,int y){//判断能不能合并
    bool up=0,down=0;
    x=find(x),y=find(y);
    for(int i=1;i<=m;i++){
        if(a[i][x].u<a[i][y].d)down=1;
        if(a[i][x].d>a[i][y].u)up=1;
        if(a[i][x].d<a[i][y].u&&a[i][x].u>a[i][y].u)return 1;
        if(a[i][x].d<a[i][y].d&&a[i][x].u>a[i][y].d)return 1;
        if(a[i][y].d<a[i][x].u&&a[i][y].u>a[i][x].u)return 1;
        if(a[i][y].d<a[i][x].d&&a[i][y].u>a[i][x].d)return 1;
    }
    return up&down;
}
queue<int>Q;
int main(){
    n=read(),m=read();
    if(m==1){//不加这个可能就只有 90,m=1 我就退化到 O(n^2) 了
        for(int i=1;i<=n;i++)
            printf("1 ");
        return 0;
    }
    for(int i=1;i<=n;i++){//预处理
        fa[i]=i;siz[i]=1;
        for(int j=1;j<=m;j++){
            a[j][i].u=a[j][i].d=read();
        }
    }
    for(int i=1;i<=n;i++){
        int x=i,maxx=0,ans;
        for(int y:S){
            if(!check(x,y))continue;
            Q.push(y);//直接删会本地就会报错,不知道为什么
            x=merge(x,y);
        }
        while(!Q.empty())S.erase(Q.front()),Q.pop();//拿出来删就好了,不差这点复杂度
        S.insert(x);
        for(int y:S){
            if(a[1][y].d>=maxx)maxx=a[1][y].d,ans=siz[y];//找最大的(这复杂度不爆???)
        }
        printf("%d ",ans);
    }
}

发完以后有人说完全看不懂,那我就结合图片讲一下?
我们把每个队各个属性的数值看成纵坐标,属性的编号看成横坐标,那么我们可以得到以这一个坐标图。

此时他们之间互不相交,彼此之间都是绝对碾压无法超过的关系,那么显然 1 就是冠军。

那此时我们加入一个 4?

此时 1 和 4 可以互相打过,那么 2 只需要在 4 打败 1 之后打败 4,那 2 也能成为冠军,那么此时可能的冠军就变成了 1,2,4。

此时 3 如果争气一点,找到一个可能打过 1,2,4 其中一个的队然后战胜它,那么 3 也能成为冠军,那么我们假设这个点是 5,因为需要 3 打过 5,那么 5 至少一个数值比 3 小,又得 5 打过 1,2,4 那么 5 必然跟 1,2,4 三条线构成的图有交。

那么就可以得到结论了,只要两个图形有交就合并,最后取其中一个数值最大的图形的大小即为答案。

B

是一个很烦的模拟题,我不想打,所以就暂且跳了吧(挖坑)。

C

原题 CF662C

题意:

给定一个 \(n\times m\) 的 \(01\) 数组,你可以同时翻转一行或者一列的状态 (\(1\)->\(0\),\(0\)->\(1\)),问翻转任意次后最少能有几个 \(1\)。

\(n\le 20,m\le 10^5\),时限 6000ms

看到 \(n\) 这么小,我们想到朴素的贪心加暴力。

关注到我们如果选定了翻的一些行和列,那么其翻转的顺序不会对最终答案造成影响,而且同一行或者列翻多次没有意义。

所以我们可以考虑 \(O(2^n)\) 枚举行的翻转状态,然后贪心看每列要不要翻,复杂度 \(O(nm2^n)\),优化一下可以跑 \(n\le15,m\le1000\) 的 \(60\) 部分分。

\(60pts\ Code\)

int n,m,ans=1e9;
bool a[20][100005],vis[20];
void dfs(int x){
    if(x>n){
        int res=0;
        for(int j=1;j<=m;j++){
            int p=0;
            for(int i=1;i<=n;i++){
                if(vis[i])p+=a[i][j]^1;
                else p+=a[i][j];
            }
            res+=min(p,n-p);
            if(res>ans)return;//优化
        }
        ans=res;
        return;
    }
    dfs(x+1);
    vis[x]=1;
    dfs(x+1);
    vis[x]=0;
}
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        string l;
        cin>>l;
        for(int j=0;j<m;j++){
            a[i][j+1]=l[j]^48;
        }
    }
    dfs(1);
    cout<<ans;
}

考虑正解。

这题可以用两个方法——dp或者FWT。这里先讲一下 dp,虽然复杂度多了一个 \(n\),但是毕竟前置知识少,所以方便写(可能不太好想到?毕竟当时好像CF赛时 tourist 都没打出来)。

注意到,每一列的很多状态很多都是重复的,而我们最终的答案相当于我们在一个全 0 的序列中翻转了一些数使得其变成了 1,求最少翻转次数。那最终我们只需要通过翻转和单点修改让每一列的状态即可,答案即为最小单点修改次数。

我们令 \(f_{i,s}\) 表示通过 \(i\) 次不重复单点修改变成状态 \(s\) 的列数。

最后答案就是枚举状态 \(s\),把对应的次数和数量相乘取最小值即可,复杂度 \(O(n^22^n)\)。

int n,m,zt[100000],f[21][1<<20];
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        string l;cin>>l;
        for(int j=0;j<m;j++)
            zt[j]=(zt[j]<<1)+(l[j]^48);//计算列状态
    }
    for(int i=0;i<m;i++)
        f[0][zt[i]]++;//状态计数
    for(int k=0;k<n;k++)//注意循环顺序,因为不能重复翻一个点,所以最外层枚举翻的点
        for(int i=k+1;i;--i)//常数优化,减少无用状态转移,同时注意从大到小,也是防止翻同一个点
            for(int j=0;j<(1<<n);j++)
                f[i][j]+=f[i-1][j^(1<<k)];
    int ans=2000000;
    for(int i=0;i<(1<<n);i++){//枚举最后所有列的状态
        int res=0;
        for(int j=0;j<=n;j++){
            res+=f[j][i]*min(j,n-j);//翻转了的答案和没翻转的取最小值
        }
        ans=min(ans,res);
    }
    cout<<ans;
    return 0;
}

FWT 做法

不想学 FWT,推个式子吧。

xor_FWT 运用:

给定长度为 \(2^n\) 两个序列 \(A,B\),设

\[C_i=\sum_{j\oplus k = i}A_j \times B_k \]

可以在 \(O(n2^n)\) 里求出 \(C\) 序列。

设翻转行的二进制操作序列为 \(opt\),\(S_i\) 为第 \(i\) 列翻转前的序列,\(T_i\) 为翻转后的序列,不能得到 \(T_i=S_i\oplus opt\)。

根据我们一开始的贪心思路,第 \(i\) 列对答案的贡献 \(ANS(T_i)=min\{popcount(T_i),n-popcount(T_i)\}\)。

那么最终答案 \(ans\) 可以表示为:

\[\large\begin{aligned}ans&=\min_{opt}\sum_{i=1}^{m}ANS(S_i\oplus opt)\end{aligned} \]

复杂度 \(O(m2^n)\)。

用 \(W(S)\) 表示状态为 \(S\) 的个数,那么:

\[\large\begin{aligned}ans&=\min_{opt}\sum_{S}ANS(S\oplus opt)\times W(S)\\&=\min_{opt}\sum_{S\oplus T=opt}ANS(T)\times W(S)\end{aligned} \]

然后就变成 xor_FWT 问题了,也就是在 \(C\) 序列里面找最小值。

D

题意:

给定一颗树,每条边的边权为 \(a+b\times delta\),问你当 \(delta\in[0,lim]\) 时,树的最小直径,并求出取到最小直径时的最小 \(delta\)。

\(n\le 2.5\times 10^5,lim \le10^6,|a_i|\le10^8,|b_i|\le10^3\)。

做法

D 好水啊,一开始没发现,暴力都没打,主要是因为之前上课都没听,不知道怎么求树的直径,然后看到要求直径就直接 skip 了。

这显然是个二分/三分题,我们把树上每条路径都拿出来,发现对于每条路径的长度,都是关于 \(delta\) 的一次函数,然后我们把这些直线放到图上,大概是长这样的:

(横坐标是 \(delta\),纵坐标是路径长度)

直径就是最长的路径,所以我们对这些线取个 \(max\),发现得到了一个半凸包(不知道是不是这么叫)。

显然不管怎样放都是一个类似单峰的折线,所以我们直接对答案进行三分就行了,三分可以参考题目:P3382 三分法

可是其实不需要像三分模板那样取三等分点,直接取中点然后比较 \(mid\) 和 \(mid+1\) 代入函数的关系:

  • 如果 \(cal(mid)<=cal(mid+1)\),那么 \(mid+1\) 一定在答案的右侧,所以我们缩小范围时就不需要 \(mid+1\) 了,直接令 \(r=mid\) 即可。
  • 如果 \(cal(mid)>cal(mid+1)\),那么发现 \(mid\) 一定不是答案,且 \(mid\) 一定在答案右侧,因此令 \(l=mid+1\) 即可。

怎么算直径呢?

树的直径通常有两种求法——搜素和树形 dp,我个人更偏向选择树形 dp 的做法,因为不仅只需要跑一遍 dfs,而且就算有负权也能保证求出正确答案。

这是因为跑两遍搜索的原理是从一点出发找到它的最长路的结束点,然后再用这个最长路的结束点去更新答案,这样做保证正确只能当没有负权边的时候成立。

证明给出:

先假设没有负权边。

我们假设从 \(A\) 点出发,能到的最远点是 \(D\) 点,而此时的直径是 \(B-C\)。

情况 1:当 \(A-D\) 和 \(B-C\) 不相交

因为 \(A\) 的最长路到的是 \(D\),所以显然 \(path:4+5\ge4+3+2\iff path:5\ge 3+2\)。

如果 \(3\) 是非负路,那么一定有 \(path:1+3+5\ge 1+2\)。

那么此时直径可以为 \(B-D\)。

情况 2:当 \(A-D\) 和 \(B-C\) 相交

这个更好证明:

这个不用管是不是负权边,肯定有 \(path:1+4\ge 1+3\iff path: 2+4\ge 2+3\),所以 \(B-D\) 为不劣解。

树形 dp 求直径

而这个题的权值可能为负数,所以显然不能两次搜素法,那就打一个简单的树形 dp 即可。

令 \(f_x\) 表示以 \(x\) 为子树的根,到子树的最长路,那么我们只要边更新 \(f_x\) 边算直径即可。

void dfs(int x,int fa){
    for(auto PI:F[x]){
        int y=PI.to;ll w=PI.v;
        if(y==fa)continue;
        dfs(y,x,v);
        len=max(len,f[x]+f[y]+w);//此时 f[x]还没有被更新,因此是与其他子树的最长路
        //那么包含 x 的最长路径就可以通过找最长和次长的路更新,再贡献到直径答案中。
        f[x]=max(f[x],f[y]+w);//更新答案
    }
}

然后就做完了。

\(Code\)

struct edge{
    int to;ll a,b;
    inline ll v(ll w){return a+b*w;}
};
int n;
vector<edge>F[250004];
ll f[250004],len,lim;
void dfs(int x,int fa,ll v){
    for(auto PI:F[x]){
        int y=PI.to;ll w=PI.v(v);
        if(y==fa)continue;
        dfs(y,x,v);
        len=max(len,f[x]+f[y]+w);
        f[x]=max(f[x],f[y]+w);
    }
}
ll cal(ll v){
    memset(f,0,sizeof(f));
    len=0;
    dfs(1,0,v);
    return len;
}
int main(){
    n=read(),lim=read();
    for(int i=1;i<n;i++){
        int u=read(),v=read(),a=read(),b=read();
        F[u].push_back({v,a,b});
        F[v].push_back({u,a,b});
    }
    ll l=0,r=lim;
    while(l<r){
        ll mid=(l+r)>>1;
        if(cal(mid)<=cal(mid+1))r=mid;
        else l=mid+1;
    }
    printf("%lld\n%lld",l,cal(l));
}

总结:

玩原神玩多了。

太菜了,在如此秒的第一题上浪费了太久,没有写出如此秒的第四题。都是玩原神玩的太菜了。

标签:24,三分法,return,int,ll,mid,dfs,答案,wzOI
From: https://www.cnblogs.com/NBest/p/17686983.html

相关文章

  • wzOI 2023.8.31 题解
    2023-09-0115:59:41$$前言$$太菜了,第一题都打成那样了没发现是MST,第三题数据范围没有很仔细看,以为是乱搞(组合之类的)题就直接跳了。不得不说这次比赛题目的一些思路还是挺妙的,一些想法确实我这种成分想不太到。$$A$$$$题意$$给出了\(m\)个可以查询的区间\([L_i,R_i]\)......
  • 实现Map批量赋值,我只需24秒搞定!
    函数的功能是将一组键值对批量赋值给Map中的键。在Java中,通常使用Map的put方法逐个将键值对赋值给Map,但在某些场景下,可能需要一次性将多个键值对赋值给Map。函数功能:Map批量赋值参数1:参数名称:target;参数类型:Map;参数描述:Map对象参数2:参数名称:keyAndValue;参数类型:Object;参数描述:k......
  • 护眼台灯怎么选?解读2024实施新国标!
    GB/T9473-2022《读写作业台灯性能要求》参编单位陪你解读护眼台灯新国标!1. 关注《读写作业台灯性能要求》对普通人来说,有什么样的意义?2. 《读写作业台灯性能要求》2022年相比2017年国标有哪些不同?3. 家长应该关注的3个选灯指标?「怎样避免孩子近视?」是家长们时刻关心的话题。想......
  • day24 - 回溯算法part01
    回溯算法理论基础 77. 组合classSolution{public:vector<vector<int>>result;vector<int>path;voiddfs(intn,intk,intstart){if(path.size()==k){result.push_back(path);return;}......
  • kafka复习:(24)consume-transform-produce模式
    packagecom.cisdi.dsp.modules.metaAnalysis.rest.kafka2023;importorg.apache.kafka.clients.consumer.*;importorg.apache.kafka.clients.producer.KafkaProducer;importorg.apache.kafka.clients.producer.ProducerConfig;importorg.apache.kafka.clients.produc......
  • 《Java编程思想第四版》学习笔记24
    //:FinallyWorks.java//ThefinallyclauseisalwaysexecutedpublicclassFinallyWorks{staticintcount=0;publicstaticvoidmain(String[]args){while(true){try{//post-incrementiszerofirsttime:......
  • 24V直流DC浪涌过压保护推荐26V电压TVS二极管
    直流DC电源端口浪涌过压防护一直都是很多新老电子工程师关注的方案之一。不管是电源端口浪涌防护还是信号接口静电保护,浪涌静电防护,找东沃,电路保护不迷路!东沃电子专注于研发、生产、销售静电保护二极管(ESD)、瞬态抑制二极管(TVS)、陶瓷气体放电管(GDT)、压敏电阻(MOV)、自恢复保险丝(PPTC)、......
  • edit-c1bad80cb9604b299cde241fca56f555.md
    ZeroTier-简单快捷组建虚拟局域网最近公司搬了新地址,开发和测试的服务器原来就在办公室放着,现在需要搬到机房,但是新的办公室和机房不在一起,网络不通。在网上找了一圈,发现了个叫ZeroTier的工具,组建局域网比较方便。官网宣传说:在任何地方安全地连接任何设备:Securelyconnec......
  • 【学习笔记】(24) 虚树
    虚树常常被使用在树形dp中,当一次询问仅仅涉及到整颗树中少量结点时,为每次询问都对整棵树进行dp在时间上是不可接受的。此时,我们建立一颗仅仅包含部分关键结点的虚树,将非关键点构成的链简化成边或是剪去,在虚树上进行dp。虚树包含所有的询问点及它们之间的lca。显然虚树的叶子节点......
  • 致远OA webmail.do 任意文件下载 CNVD-2020-62422
    漏洞描述致远OA存在任意文件下载漏洞,攻击者可利用该漏洞下载任意文件,获取敏感信息影响版本致远OAA6-V5致远OAA8-V5致远OAG6漏洞复现fofa语法:app="致远互联-OA"登录页面如下:致远OAwebmail.do文件读取漏洞,由于/seeyon/webmail.do页面filePath参数过滤不严,导致可以......