【四校训练赛二】总结收获与多题多解

本文回顾了一场比赛经历,总结了比赛中的得失,并详细解析了几道典型题目,包括进制转换问题的不同解决方法、大规模数据处理技巧及TwoStack算法的应用,还有离线并查集在图论问题中的巧妙运用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

PART #1:

题目地址
这里写图片描述

PART #2:

个人总结,读者可跳过】四校训练赛第二场,比赛时一小时出4道题,其中有一道CF的原题,之前做过的还WA了两发很不应该,然后一道简单的概率题写20分钟之久需提高。到比赛一个小时后之后的思维清晰度下降,下次需要注意(可能是中午没有午睡的原因?)。共有十道题,按照正常发挥应该1小时出5道,其余5道题从中学到不少东西。感谢出题方的创意和志愿工作。

PART #3:

个人向题解】在FJUT OJ的STATUS中有比我更好的方法,以下题解仅供参考。

FJUT 3258 benTuTuT的进制转换

错误方法:一开始看到题的想法是贪心暴力从前往后走一遍,每次取小于n的数字,然后对答案进行更新,具体的操作如下:对于样例 17 10086,从头开始去,一开始取到数字1(1<17),继续取10(10<17),此时不能继续往后取,否则会超过17,此时更新res = 10,而后得到数字0,由于0不能做高位,所以res = res * 17表示提高一位,而后取到8,res =res*17+8,最后取到6,res =res*17+6,得到结果835352.对于所有给出的样例来说都是成立。但是这有一个坑点,试想12 111 按此方法得到就是11*12+1,然而最小的是1*12+11。
正确方法之一:倒序完成上述操作,由刚才的分析可以得到,每次贪心最多的位数总能使得最后结果的位数较小,由12 111可以得到从后贪心的正确性。
正确方法之二:依旧是顺序进行上述操作,唯一的不同是DP的方法来完成这些操作,对于每一个DP[i]表示的是,前i位的数字得到的最小结果,每增加一位我们就可以对答案进行更新,取最小的结果。注意:在运算的过程中,可能会爆long long,所以需要提前注意处理特殊的情况。
正确方法之一代码:(贪心,倒序)

#include<bits/stdc++.h>
using namespace std;

#define ll long long
#define CLR(x) memset(x,0,sizeof(x))

const int maxn = 500;

ll n;
int pos,vis[maxn];
string s;
vector<ll>V;

int main()
{
    while(cin >> n >> s){
        V.clear(); CLR(vis);

        for(int i=s.size()-1; i>=0; i--){
            if(vis[i]) continue; //对于已经使用过的数字直接跳过

            ll x = 0, tem, k = 0;

            for(int j=i;j>=0;j--){
                tem = (ll)(s[j]-'0')*(ll)round(pow(10,k++))+x;//计算当前取到的数值
                if(tem >= n|| k>=18) break;//注意如果这里缺少k>=18的判断就会溢出(爆long long)
                x = tem;

                if(s[j]-'0'){//取到合法的数字后将使用的数字都标记
                    pos = j;
                    while(!vis[pos]&&pos<s.size()) vis[pos++]=1;
                }
            }
            V.push_back(x);
        }

        ll res = 0;
        for(int i=V.size()-1; i>=0; i--) res = res*n + V[i];//获得10进制结果
        cout<<res<<endl;
    }
    return 0;
}


正确方法之二代码:(DP,顺序)

#include<bits/stdc++.h>
using namespace std;

#define ll long long

const ll LINF = 1e18;
const int maxn = 200+10;

ll m,dp[maxn],n;
string str;

ll mul(ll a,ll b){
    if(a >= LINF / b)return LINF;//注意会爆long long
    return a*b;
}

ll sum(ll a,ll b){
    if(a >= LINF - b)return LINF;//注意会爆long long
    return a + b;
}

int main()
{
    while(cin>>n>>str)
    {
        fill(dp,dp+maxn,LINF);
        dp[0]=0;
        for(int i=0; i<str.size(); i++){
            if(str[i]!='0'){
                ll x = 0;
                for(int j=i; j<str.size(); j++){
                    x=x*10+str[j]-'0';
                    if(x>=n)break;
                    dp[j+1] = min(dp[j+1],sum(mul(dp[i],n),x));
                }
            }
            else dp[i+1] = min(dp[i+1],mul(dp[i],n));
        }
        cout<<dp[str.size()]<<endl;
    }
    return 0;
}

.
.

FJUT 3260 Anxdada的询问(hard)

分析:【先来BB两句,要看核心的请跳到下一段】在3259的这个easy版本中,主要是因为数据范围比较小,所以可以直接暴力,但是这个版本的数据量非常大,一个测试样例的数据就有高达 3e6 3 e 6 的数据量,题目中要求是最好使用读入输出外挂,实际上使用了以后会快三分之一的时间,对于正确算法的影响不大,对于一般算法的影响较大。

首先这个题一开始想到的是线段树的方法,但是线段树的方法的复杂度是 OQlog(N) O ( Q · l o g ( N ) ) 的,对于大规模数据来说( N=1e6,Q=1e6 N = 1 e 6 , Q = 1 e 6 )是很容易超时的,实际上出题人在出线段树的解时考虑到这一点,特意卡掉了线段树的写法,但是仍然有一个线段树过了,有其过人之处。

再然后会想到用尺取法的方式去做,但是问题是如果要去除上一个区间的某一部分时,如果模数p是合数的话,取逆元的方法是不成立的(这也是我比赛的时候一直没有注意的地方),所以尺取法也要PASS。

注意到,这个题目有个特殊的地方是,每次询问保证询问的区间会在之前询问的后面。于是就有了黑科技的登场:Two Stack。实验室的另外一位朋友也写过Two Stack的做法,如果各位觉得我讲得不好,可以再去他那里去看看地址点击这里

这个Two Stack用来维护两个区间的关系,对于不同的题,关系也不同。对于本题来说,就是维护两个区间的乘积关系。那么它是怎么维护具体的区间的呢,我们可以看图说明。

首先我们有第一个区间要更新了,假设我们第一次更新的区间为 [l1,r1] [ l 1 , r 1 ] 则如图,推入S1中的依次是 l1,l1(l1+1),...,l1(l1+1)(r11)r1 l 1 , l 1 · ( l 1 + 1 ) , . . . , l 1 · ( l 1 + 1 ) · · · ( r 1 − 1 ) · r 1 这个栈的用处是用来维护顺序的乘积情况,同时它只负责对于扩展的新区间产生的乘积项的维护,具体表现之后会画图表现。
维护第一个区间的情况
对于第一个区间的更新情况如上,此时结果就为S1栈顶的元素,当更新新的区间 [l2,r2](l2>l1,r2>r1) [ l 2 , r 2 ] ( l 2 > l 1 , r 2 > r 1 ) 时,我们先选择扩展S1,同样的方法我们将S1的栈顶扩展到 ,l1(l1+1)(r11)r1(r21)r2 , l 1 · ( l 1 + 1 ) · · · ( r 1 − 1 ) · r 1 · · · ( r 2 − 1 ) · r 2 然后将所有的元素的倒序相乘倒入S2中,同时清空S1,如下图所示:
更新第二个区间时扩展的情况
这里写图片描述
然后再执行POP操作,POP元素直到S2的栈顶为 r2(r21)(l2+1)l2 r 2 · ( r 2 − 1 ) · · · ( l 2 + 1 ) · l 2 如下图所示:
这里写图片描述
通过不断得扩展和POP,我们就能不断得更新新的区间。

一定要注意的是,S1只负责扩展右边的新区间,而S2只负责POP掉左边的旧区间,同时他们的顺序是相反的,一定要注意观察细节!细节!细节!(重要的事说三遍)

正确方法之一代码:(Two Stack法)

#include<bits/stdc++.h>
using namespace std;

#define ll long long
const int maxn = 1e6+100;

int n,p,T1;
ll nums[maxn];

ll Scan()//读入外挂
{
    ll res=0,ch,flag=0;
    if((ch=getchar())=='-')
        flag=1;
    else if(ch>='0'&&ch<='9')
        res=ch-'0';
    while((ch=getchar())>='0'&&ch<='9')
        res=res*10+ch-'0';
    return flag?-res:res;
}
void Out(ll a)//输出外挂
{
    if(a>9)
        Out(a/10);
    putchar(a%10+'0');
}

stack<ll>s1,s2;

void pop(){//POP左区间
    if(s2.empty()){
        while(!s1.empty()){
            if(s2.empty())s2.push(nums[T1--]%p);
            else s2.push(nums[T1--]*s2.top()%p);
            s1.pop();
        }
    }
    s2.pop();
}

int main()
{
    int q;
    n=Scan();p=Scan();
    for(int i=1;i<=n;i++){nums[i]=Scan();}
    q=Scan();

    int l,r,prel=1,prer=0;
    T1 = -1;//T1用来记录最后一个被推入S1的元素下标

    for(int i=0;i<q;i++){
        l=Scan();r=Scan();
        ll res = 0;

        for(int j=prer+1;j<=r;j++){//扩展右区间
            if(s1.empty())s1.push(nums[j]%p);
            else s1.push(s1.top()*nums[j]%p);
        }
        T1=r;

        for(int j=prel;j<l;j++) pop();

        if(s1.empty()) res = s2.top()%p;
        else if(s2.empty()) res = s1.top()%p;
        else res = s1.top()*s2.top()%p;

        Out(res); putchar('\n');
        prel=l,prer=r;
    }
    return 0;
}

正确方法之二代码:(优秀的线段树)

#pragma GCC optimize(2)
#pragma G++ optimize(2)
#include<bits/stdc++.h>
#define ll long long
#define lson l,mid,rt<<1
#define rson mid+1,r,rt<<1|1

const int INF = 0x3f3f3f3f;
const long long LINF = 0x3f3f3f3f3f3f3f3fll;
using namespace std;

const int maxn = 1e6+100;

ll Scan()//读入外挂
{
    ll res=0,ch,flag=0;
    if((ch=getchar())=='-')
        flag=1;
    else if(ch>='0'&&ch<='9')
        res=ch-'0';
    while((ch=getchar())>='0'&&ch<='9')
        res=res*10+ch-'0';
    return flag?-res:res;
}
void Out(ll a)//输出外挂
{
    if(a>9)
        Out(a/10);
    putchar(a%10+'0');
}

ll n,m,p,sum[maxn<<2],q;
void build(int l,int r,int rt){
    if(l==r){
        sum[rt]=Scan();
        sum[rt]%=p;
        return ;
    }
    int mid = (l+r)/2;
    build(lson);
    build(rson);
    sum[rt]=sum[rt<<1]*sum[rt<<1|1]%p;
}

ll query(int L,int R,int rt,int l,int r){
    if(L>=l&&R<=r) return sum[rt];
    int mid = (L+R)/2;
    if(l>mid)   return query(mid+1,R,rt<<1|1,l,r);
    else if(r<=mid) return query(L,mid,rt<<1,l,r);
    else return query(L,mid,rt<<1,l,mid)*query(mid+1,R,rt<<1|1,mid+1,r)%p;
}

int main()
{
    n=Scan();
    p=Scan();
    build(1,n,1);
    q=Scan();
    while(q--){
        ll l,r;
        l=Scan(),r=Scan();
        Out(query(1,n,1,l,r));
        putchar('\n');
    }
    return 0;
}

FJUT 3262 垃圾佬的旅游II

分析:一开始刚看着道题会觉得是图论的算法,一看这么复杂感觉有点恐怖。然后仔细一想,由于它只是计算两点之间路径的上的最大权值,而不是权值之和(这两者的区别请读者代入题目自行领悟),瞬间就懂了,原来是披着“毒瘤题”的水题。
具体做法是离线并查集:

首先:将所有的边储存下来,然后将其排序,然后对所有的询问都储存下来,然后将其排序,这样的好处就能够一直使用前几个询问的答案而不用在线处理浪费时间,尤其是在这个题的询问是 2e5 2 e 5 次的情况下。

之后:按顺序将小于等于询问的边添加进图中。注意到由于答案要求是点对的个数,如果添加进的两个点本身就是相连接的那么,答案和上一次询问相同。否则对答案的贡献就是 number(group(v))number(group(u)) n u m b e r ( g r o u p ( v ) ) ∗ n u m b e r ( g r o u p ( u ) ) ,其中 group(v),group(u) g r o u p ( v ) , g r o u p ( u ) 分别代表的就是两个互不相连的集合。大家可以画个图看一下。

#include<bits/stdc++.h>
#define ll long long
using namespace std;

const int maxn = 2e5+100;

struct node{
    int u,v,len;
    bool operator < (const node &b) const{
        return len<b.len;
    }
}p[maxn];

struct NODE{
    int id;
    int val;
    bool operator <(const NODE &b)const{
        return val<b.val;
    }
}q[maxn];

int pre[maxn];
int n,m,Q;
ll ans[maxn];

int Find(int x){
    return pre[x] < 0 ? x : pre[x] = Find(pre[x]);
}

ll Union(int x,int y){//合并操作
    int X = Find(x);
    int Y = Find(y);
    if( X == Y) return 0;
    else {
        int tem = pre[X] + pre[Y];
        ll res = (ll) pre[X] * (ll) pre[Y];//这里记录了两个集合点的个数的积
        if(X>Y) pre[X] = Y, pre[Y] = tem;
        else pre[Y] = X, pre[X] = tem;
        return res;
    }
}

void ini(){
    for(int i=0;i<=n;i++){
        pre[i]=-1;
    }
}

int main()
{
    scanf("%d%d%d",&n,&m,&Q);
    ini();//别忘记初始化
    for(int i=0;i<m;i++) scanf("%d%d%d",&p[i].u,&p[i].v,&p[i].len);
    sort(p,p+m);
    for(int i=0;i<Q;i++) {scanf("%d",&q[i].val);q[i].id=i;}
    sort(q,q+Q);

    int pos = 0;
    ll res = 0;
    for(int i=0;i<Q;i++){
        while(p[pos].len<=q[i].val&&pos<m){
            res += Union(p[pos].u,p[pos].v);
            pos++;
        }
        ans[q[i].id]=res;//要将结果储存在答案数组中对应的位置
    }
    for(int i=0;i<Q;i++){
        cout<<ans[i]<<endl;
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值