Loading

杂题

幽夜华尔兹

一个直观的思想是,用线段树进行区间加然后查询最大值,结果直接被样例hack了。考虑错误的地方---无法判断是两个区间各覆盖一半还是一个区间覆盖完。换个思路,记下是哪个区间覆盖的,发现信息量太大。发现瓶颈是错误的地方+枚举毫无顺序,由csp-s2024T2的区间问题,想到我们应钦定一个顺序,即按左端点排序,那么从左到右考虑每个区间,记当前区间为i,发现如果有个区间为j想覆盖i那么\(j_r>i_l,j_l<i_r\)必须至少一个成立,那么分两种情况
1.j>i,考虑直接找到最后一个j使得\(j_l<=i_r\),但是发现有时候\(j_r<i_l\)了怎么办,这就是刚才sort的作用,我们只考虑j>i的,所以\(j_l\)一定>=\(i_l\)
2.j<i,那么发现对于\(\forall i,i_l\leqslant j_r\)都可以贡献1,而合法的i此时又是一个区间,那么直接用树状数组维护即可
综上,只要sort一边,每次执行操作1,然后执行操作2,就可以解决此题,具体看代码

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define N_ 200005
const ll inf = 1e9;
#define P pair<ll, ll>
struct node {
    ll l, r;
} t[N_];
ll v[N_], n, ans;
void add(ll x, ll val) {
    for (; x <= n; x += x & -x) v[x] += val;
}
ll ask(ll x) {
    ll res = 0;
    for (; x > 0; x -= (x & -x)) res += v[x];
    return res;
}
bool operator<(const node a, const node b) {
    if (a.l == b.l)
        return a.r < b.r;
    return a.l <= b.l;
}
int main() {
    freopen("waltz.in", "r", stdin);
    freopen("waltz.out", "w", stdout);
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> t[i].l >> t[i].r;
    sort(t + 1, t + 1 + n);
    for (int i = 1; i <= n; i++) {
        node o = { t[i].r, inf };
        ll c = upper_bound(t + 1, t + 1 + n, o) - t;
        ll d = c - i - 1;
        ll sum = ask(i);
        add(1, 1);
        add(c, -1);
        ans = max(d + sum, ans);
    }
    cout << ans;
}

一个不好的扫描线题P3117 [USACO15JAN] Cow Rectangles G(甚至都懒得给链接)
似乎什么方法都能做,还有人用随机数A的.还有种笔者口胡的做法,考虑到,其实一个矩形内有多少个点和是否有坏点是可以用二维前缀和O(1)维护的,那么如果枚举边长就是\(O(n^4)\),考虑如何优化,其实可以二分答案,长,宽,然后枚举矩形的左下端点即可,离散化一下就是\(O(nlog^3n)\).
接下来讨论正解:因为是"扫描线"我们可以枚举一下矩形的一组对边的横坐标,然后另一个对边暴力扫描
+双指针(因为有单调性).因为注意到其实矩形的边一定在好点上(因为面积最小),所以离散化一下就是\(O(n^3)\)

P2827 [NOIP 2016 提高组] 蚯蚓

第一眼看到这题似乎很简单,直接一个堆秒了,至于增量q,每次入堆的时候记一下入堆时间,然后用现在时间减一下就好了时间复杂度\(O(mlog(m+n)\)m是7X1e6,大概是1.61X1e8时间比较紧,但卡卡好像可以过?
考虑时间比较宽松的正解,思考为什么我们要堆,因为我们需要快速求最大值,那堆的瓶颈又是什么,就是要log才能求最大值,出题人数据明显是希望我们\(O(1)\)求最大值,据我所知没有O(1)动态求最大值的数据结构,那么我们猜测这个可能有单调性

首先肯定先sort,如果一个蚯蚓被砍断了,那么肯定会减小,我们不可能O(1)插回去,那么考虑开个que,存第一段,考虑证明这个有单调性设原序列有两个数\(x_1,x_2,x_1>=x_2\),那么比较一下第一段的大小(因为不能同时砍两只,所以要+q)那么x1第一段是\(x_1*p+1\),x2的第一段是\((x_2+q)*p=x_2*p+q*p\)显然x1第一段仍大于x2第一段,第二段同理,但是x2第1段和x1第2段难比较,那么在开一个que存的二段
最后求答案就是三个que的顶,至于+q的话随便维护就好了
注意:算切两半过程中要开long double,输出格式注意一下
小结:遇见新题,划归转化,找到瓶颈大胆猜想,没有思路猜想出题人考什么,注意细节

P1631 序列合并

一个简单二分题,但笔者磕了好久(就是菜呗)
这明显是二分,之前我们已经总结过了二分题目的基本形式,现在转换成如何求第N大,但是发现有一些困难,因为我们不可能枚举两两个数,比较和mid谁大,发现瓶颈是枚举两两个数,我们考虑优化,因为数组a,b均有单调性,我们可以直接考虑lower_bound,这样即可.
但是发现还是可以优化,我们从瓶颈出发,发现有些数其实更本不必要枚举,因为如果一个bi+ai比mid大 那么\(b_i+a_{i+1}>mid\),所以我们考虑直接用双指针,就可一优化调一个log,然后考虑如何统计答案,同checkmid一样即可,注意一些细节等于号什么的

小结:二分要仔细考虑=什么的细节,然后对于有单调性的题目,可以考虑用倍增,二分,双指针等维护,有时后还可以叠加

P130019 地球往事

首先先考虑暴力,那么是\(N^2\)的,思考一下瓶颈在哪,发现是限制很烦,又有i,又有j,使得我们不得不枚举两个量,考虑分离变量,令\(i-a_i\)为\(A_i\) \(b_i-i\)为\(B_i\)发现变成要满足$ B_i<=B_j &&A_i<=A_j $,这时候变成一个二维问题,我们想到画图.发现左下和右上可以合成一个连通块.
一般暴力就是枚举顺序不优秀,我们现sort,以B为横坐标,那么发现合并可以用一个递减单调栈维护,如果一个点小于栈顶那么就是无法合并,加入.否则可以将栈中小于等于它的数都合并,然后将最小的扔回栈顶就好了,答案就是栈大小
小结:遇见多元问题考虑分离变量,对于二维问题画图,优化暴力->改变枚举顺序

P130042 grid

题意有些难懂,但是理解后比较简单.首先画个图发现如果每次路径正好多包含一个最优,那么答案就是障碍数+1,但是发现有些障碍(比如相连的)只能算一个障碍,那么要把他们合并,合并操作是简单的.
注意我们发现如果连通块在第1,N行第1,M列不能算进去,那么最终我们还要减去;

P130155 平衡树

结论题.
考虑要每棵子树都一样大,那么如果直接做肯定会炸,假设我们已经推出一个点是平衡点(这是好做的),如何推广?
根据想到这比较像换根dp如果一个点的是平衡点,那么我们往它儿子走,其他儿子是一样的,就只要判断产生的小变化,似乎就行了.现在瓶颈在于如何求出一个平衡点,做这种T1,T2,一般都是结论题,要勇敢的连蒙带猜.因为它的儿子是一样大小,我们猜它是重心,因为重心儿子大小最可能相等.但是我们注意到重心可能不只有一个,而且重心又是我们猜的,会不会不是重心?
如果在这里不敢往下想就前功尽弃了,我们再找找性质.发现如果一个点是符合条件,并且至少有两个儿子,那么儿子的siz必须<n/2.想起一个结论如果存在一个节点v,使得删除v后所有子树大小严格小于n/2,那么v是树的唯一重心。
那么猜想正确,如何转移能,根据部分分发现,似乎只可能是叶子,那么考虑先判断重心是不是,如果是往叶子走,如果到叶子中间的节点都没有分叉,那么肯定叶子也是.
小结:大胆猜结论,大概是对的就写代码,不需急切证明以IDA*思想写题.结论如果存在一个节点v,使得删除v后所有子树大小严格小于n/2,那么v是树的唯一重心。

P6572 [BalticOI 2017] Railway

虚树,差分,启发式合并题,以下给出三种解法:

1.

首先我们考虑如何暴力,如果不考虑重复就是一个树上差分,对于重复就是因为枚举顺序出问题,同时这也是TLE的重要原因,那么现在目标转移到如何优化枚举顺序,树上问题唯一可以有顺序的就是dfs序和欧拉序,那么肯定是选dfs(因为简单)。
我们根据dfs排序,然后对于每个相邻的做一次,发现似乎有些边有两次,有些边只有一次,没有规律。但是如果细心一点就会发现,如果我们把第一个和最后一个也做一次,那么每条边正好被重复计算一次。

2.

首先看见一个\(\sum s_i\)可能是虚树,我们发现瓶颈就在于会重复遍历——进入一棵子树,有可能因为有关键点在外面,又要出去.
那么先尝试建出虚树,然后我们发现一些美好性质——树上关键点与它们lca之间已连边,只要在虚树递归时候做就可以.具体的,遍历到一个点时将sum[u]++,sum[f]--,说白了就是个树上差分。

3.

你先别急
小结:
1.暴力优化可以考虑枚举顺序,树上找顺序——dfs序.
2.总操作点数不大可以用虚树.

P2221 [HAOI2012] 高速公路

首先考虑如何暴力,对于这种连续求和,一个常见的trick是拆贡献,有一个l,r,对于一条在l,r之中的边i,我们思考它会被贡献多少次,发现在i左边的收费站,和i右边的收费站,两两配对都有贡献.
那么写成式子就是\(\sum_{i=l}^{r}(i-l+1) * (r-i) * v_i\),发现,i和其他常数结合一起,考虑分离常数
dirty work后就可以写成

\[\begin{pmatrix} (r-l * r) * \sum_{i=l}^{r-1}v_i+(r+l-1) * \sum_{i=l}^{r-1}v_i * i - \sum_{i=l}^{r-1}v_i * i * i \end{pmatrix} \]

发现,其实变量就是v[i]和v[i] * i 和 v[i] * i * i,可以用线段树维护,对于每次加法,v[i]直接加,另外两个自己dirty work一下吧.
注意开long long

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define N_ 200005
#define P pair<ll,ll> 
struct node{
    ll i0,i1,i2;
    node operator + (const node& x)const {
        return {i0+x.i0,i1+x.i1,i2+x.i2};
    }
}v[N_*4];
ll N,M,tap[N_*4],ls[N_*4],rs[N_*4],H[N_],Node,root,h[N_];
void push_up(ll cnt)
{v[cnt]=v[ls[cnt]]+v[rs[cnt]];}
void push_down(ll cnt,ll p,ll q)
{
    ll mid=p+q>>1;
    if(tap[cnt]!=0)
    {
        ll val=tap[cnt];
        tap[cnt]=0;
        if(1)
        {
            if(!ls[cnt])ls[cnt]=++Node;
            ll l=p,r=mid;
            v[ls[cnt]].i0+=(r-l+1)*1ll*val;
            v[ls[cnt]].i1+=val*1ll*(h[r]-h[l-1]);
            v[ls[cnt]].i2+=val*1ll*(H[r]-H[l-1]);
            tap[ls[cnt]]+=val;
        }
        if(1)
        {
            if(!rs[cnt])rs[cnt]=++Node;
            ll l=mid+1,r=q;
            v[rs[cnt]].i0+=(r-l+1)*1ll*val;
            v[rs[cnt]].i1+=val*1ll*(h[r]-h[l-1]);
            v[rs[cnt]].i2+=val*1ll*(H[r]-H[l-1]);
            tap[rs[cnt]]+=val;
        }
    }
}
void add(ll& cnt,ll l,ll r,ll p,ll q,ll val)
{
    if(!cnt)cnt=++Node;
    // cout<<l<<" "<<r<<endl;
    if(p<=l&&r<=q)
    {
        v[cnt].i0+=(r-l+1)*1ll*val;
        v[cnt].i1+=val*1ll*(h[r]-h[l-1]);
        v[cnt].i2+=val*1ll*(H[r]-H[l-1]);
        tap[cnt]+=val;
        return ;
    }
    push_down(cnt,l,r);
    ll mid=l+r>>1;
    if(p<=mid)add(ls[cnt],l,mid,p,q,val);
    if(q>mid)add(rs[cnt],mid+1,r,p,q,val);
    push_up(cnt);
}
node ask(ll cnt,ll l,ll r,ll p,ll q)
{
    node ans={0,0,0};
    if(!cnt)return ans;
    if(l>=p&&r<=q)
        return v[cnt];
    push_down(cnt,l,r);
    ll mid=l+r>>1;
    if(p<=mid)ans=ans+ask(ls[cnt],l,mid,p,q);
    if(q>mid)ans=ans+ask(rs[cnt],mid+1,r,p,q);
    push_up(cnt);
    return ans;
}
int main()
{
    for(int i=1;i<=1e5+10;i++)
    H[i]=H[i-1]+i*1ll*i;
    for(int i=1;i<=1e5+10;i++)
    h[i]=h[i-1]+i;
    cin>>N>>M;
    for(int i=1;i<=M;i++)
    {
        char ch;
        cin>>ch;
        ll l,r,val;
        while(isspace(ch))cin>>ch;
        if(ch=='C'){
            cin>>l>>r>>val;
            r--;
            add(root,1,N-1,l,r,val);
        }
        if(ch=='Q'){
            cin>>l>>r;
            ll ans=0;
            node res=ask(root,1,N-1,l,r-1);
            ans=res.i0*1ll*(r-l*r)+res.i1*1ll*(r+l-1)-res.i2;
            ll G=__gcd(ans,(r-l+1)*1ll*(r-l)/2);
            cout<<ans/G<<"/"<<(r-l+1)*1ll*(r-l)/2/G<<'\n';
        }
    }
    return 0;
}

小结:一个数学题,都是常见trick

首先考虑哪些可以給出贡献,必然是在询问之前的操作,并且这个操作的区间要包含询问区间,而且对于每个字符串相对顺序都要一样,但是注意到每次加入的数都两两不同,设询问区间为A,那么题目也就变成:$\sum_{i=1}^{Q} (A \in B_i且T_B<T_A) $其中&T_B&表示区间B出现时间,那么这就是一个三维偏序模板,随便做就好

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define N_ 100005
#define M_
void read(ll& x)
{
    char ch=getchar();ll f=1;x=0;
    while(ch<'0'||ch>'9'){
        if(ch=='-')f=-1;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9'){
        x=x*10+ch-48;
        ch=getchar();
    }
    x=x*f;
}
struct node{
    ll l,r,op,t;
}t[N_];
ll ans[N_],N,Q,c[N_];
bool vis[N_];
void add(ll x,ll val)
{
    for(;x<=1e5;x+=x&-x)c[x]+=val;
}
ll ask(ll x)
{
    ll res=0;
    for(;x>0;x-=x&-x)res+=c[x];
    return res;
}
void cdq(ll l,ll r)
{
    if(l==r)return ;
    ll mid=l+r>>1;
    cdq(l,mid);cdq(mid+1,r);
    sort(t+l,t+1+mid,[](node x,node y){return x.l<y.l;});
    sort(t+mid+1,t+1+r,[](node x,node y){return x.l<y.l;});
    ll left=l;
    for(int i=mid+1;i<=r;i++)
    {
        while(left<=mid&&t[left].l<=t[i].l)
        {
            if(t[left].op==1)
                add(t[left].r,1);
            left++;
        }
        if(t[i].op==2)ans[t[i].t]+=ask(1e5)-ask(t[i].r-1);
    }
    for(int i=l;i<left;i++)
    if(t[i].op==1)
            add(t[i].r,-1);
}
int main()
{
    read(N);read(Q);
    for(int i=1;i<=Q;i++)
    {
        read(t[i].op);read(t[i].l);read(t[i].r);t[i].t=i;
        if(t[i].op==2)vis[i]=1;
    }
    cdq(1,Q);
    for(int i=1;i<=Q;i++)
    if(vis[i])cout<<ans[i]<<'\n';
    return 0;
}

P3120

首先考虑到贡献必须满足\(x_i<x_j,y_i<y_j,v_i!=v_j\),显然cdq,那么我们需要把矩形压成一个区间,但是我们注意到,cdq只能做<=.而<我们需要减去=的,需要容斥比较复杂.
法一:
考虑到可以支持n*n,而我们的瓶颈是不支持不取等,那么我们可以想到,造成这种情况是应为区间没办法保证\(x_i<x_j\)那么我们可以直接在矩形上做,可以对部分进行cdq.具体的,我们对x进行cdq,这样就可保证\(x_i<x_j\)就可以解决问题
法二:
可以发现每一行内部无法转移,那么终止条件可以修改为r-l<=M

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define P pair<ll,ll> 
#define N_ 5005

void read(ll& x)
{
    char ch=getchar();ll f=1;x=0;
    while(ch<'0'||ch>'9'){
        if(ch=='-')f=-1;
        ch=getchar();
    }
    while(ch>='0'&&ch<='9'){
        x=x*10+ch-48;
        ch=getchar();
    }
    x=x*f;
}
const ll  p = 1000000007;
ll t[N_][N_];
ll N,M,K,dp[N_][N_],tot,c[N_];
void cdq(ll l,ll r)
{
    if(l==r)return; 
    ll mid=l+r>>1;
    cdq(l,mid);ll sum=0;
    for(int j=1;j<=M;j++)
    {
        for(int i=mid+1;i<=r;i++)
        {
            dp[i][j]+=sum;dp[i][j]%=p;
            dp[i][j]=(dp[i][j]-c[t[i][j]]+p)%p;
        }
        for(int i=l;i<=mid;i++)
        {
            sum+=dp[i][j];sum%=p;
            c[t[i][j]]+=dp[i][j];c[t[i][j]]%=p;
        }
    }
    memset(c,0,sizeof(c));
    cdq(mid+1,r);
}   
int main()
{
    read(N);read(M);read(K);
    for(int i=1;i<=N;i++)
        for(int j=1;j<=M;j++)read(t[i][j]);
    dp[1][1]=1;
    cdq(1,N);
    cout<<dp[N][M];
    return 0;
}

小结:具体情况具体分析,这题我们可以发现每一行内部无法转移

CF1793F

方法一:

题目給出一堆区间,然后维护的又是不好合并差分的信息,考虑莫队。
容易发现加入一个数是更新答案是简单的,log 的时间就可以解决,但是删除一个数是困难的,所以考虑回滚莫队
又因为log加入肯定会炸,还要优化,套路的,二次离线下来,用一个线段树扫描线一下就好。
时间复杂度\(O(n \sqrt q)\)

方法二:

仍然考虑暴力的做,用根号分治。
首先为了可以统一处理,我们把同r的放到一个vector里面。

对于len<sqrt(n)

因为我们已经固定了r,所以可定是要在l上作文章,根号分治后,对于区间就可以暴力维护了,维护出\(f_i\)表示从[i,r]的最小差值,更新就是加入一个r然后暴力更新[r-B+1,r-1]的f就好。

对于len>sqrt(n)

虽然没法对区间进行维护了,但是我们发现更好分治后答案一定小于sqrt(n),所以我们记\(g_i\)表示以r结尾,答案是v,的左端点至少是\(g_i\)。先于处理出每个点的位置,更新就是加入一个右端点,然后我们暴力在值域上\(j \in [a[r]-B,a[r]+B]\),更新g[j-a[r]]=min(g[j-a[r]],pos[j])。然后因为有单调性,所以对g做一个前缀最大值,然后查询的时候双指针。
小结:1.题目給出一堆区间,然后维护的又是不好合并差分的信息,考虑莫队。2.与排列有关,又是不好合并差分的信息,根号分治

posted @ 2025-11-16 22:30  leaf_ydc  阅读(5)  评论(0)    收藏  举报