good problems-8

1.E - Red and Blue Graph
题意:n个点m条边的图,选k个点染色,使得特殊边的个数为偶数,特殊边指连接这条边的2点一个染色一个未染色
题解:

由此可见,特殊边的奇偶性和选的k个点的度数和的奇偶性相同
那么我们进行枚举,枚举奇数度数的点选x个,偶数度数的点选k-x个

点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#include<set>
#define ll long long 
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=2e6+101;
const int MOD=998244353;
const ll inf=2147483647;
const double eps=1e-12;

ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}
ll power(ll x,ll y){
    ll ans=1;
    while(y){
        if(y&1)ans=ans*x%MOD;
        y>>=1;x=x*x%MOD;
    }
    return ans;
}
ll fac[maxn],inv[maxn];
ll C(ll n,ll m){
    if(m>n)return 0;
    if(m==0 || n==m)return 1ll;
    ll ans=fac[n]*inv[m]%MOD;
    ans=ans*inv[n-m]%MOD;
    return ans;
}
ll n,m,k,in[maxn];
int main(){
    n=read();m=read();k=read();
    fac[0]=1ll;
    for(ll i=1;i<=n;i++)fac[i]=fac[i-1]*i%MOD;
    inv[n]=power(fac[n],MOD-2); 
    for(ll i=n-1;i;i--)inv[i]=inv[i+1ll]*(i+1ll)%MOD;

    for(int i=1;i<=m;i++){
        int x=read(),y=read();
        in[x]++;in[y]++;
    }

    ll cnt1=0,cnt2=0;
    //cnt1奇数度数个数
    for(int i=1;i<=n;i++){
        if(in[i]&1)cnt1++;
        else cnt2++;
    }
    ll ans=0;
    for(ll i=0;i<=min(k,cnt1);i+=2){
        ans=ans+C(cnt2,k-i)*C(cnt1,i)%MOD;
        //可能k-i大于cnt2
        ans%=MOD;
    }
    cout<<(ans%MOD+MOD)%MOD;
    return 0;
}

2.D. Magical Array
题解

点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#include<set>
#define ll long long 
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=2e6+101;
const int MOD=998244353;
const ll inf=2147483647;
const double eps=1e-12;

ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}

void solve(){
    int n=read(),m=read();
    vector<ll>sum(n);
    for(int i=0;i<n;i++){
        ll pre=0;
        for(int j=1;j<=m;j++)pre+=read(),sum[i]+=pre;
    }
    ll mx=*max_element(sum.begin(),sum.end());
    ll mi=*min_element(sum.begin(),sum.end());
    int pos=min_element(sum.begin(),sum.end())-sum.begin()+1;
    printf("%d %lld\n",pos,mx-mi);
    return ;
}

int main(){
    int t=read();
    while(t--)solve();
    return 0;
}

3.E. Count Seconds


用拓扑排序求dp


因为根据结论1,当一个点由点权大于0变成0后一直都是0
但存在图中并没有流到的点,也就是存在0--》大于0--》0
我们dp时只能根据 大于0--》0来进行dp,因为我们没法计算0--》大于0的时间
其实dp式子成立要保证一流一出
可以画一个链来模拟试试

点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#include<set>
#define ll long long 
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=2e6+101;
const int MOD=998244353;
const ll inf=2147483647;
const double eps=1e-12;

ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}
ll n,m,a[maxn],in[maxn];
int tot,head[maxn],to[maxn],nx[maxn];
void add(int x,int y){
    to[++tot]=y;nx[tot]=head[x];head[x]=tot;
}
void solve(){
    n=read();m=read();
    int cnt=0;
    for(int i=1;i<=n;i++){
        a[i]=read();in[i]=head[i]=tot=0;
        if(a[i])cnt++;
    }
    for(int i=1;i<=m;i++){
        int x=read(),y=read();
        in[y]++;add(x,y);
    }
    if(!cnt){puts("0");return ;}
    for(int k=1;k<=n;k++){
        vector<int>b(n+1);
        for(int i=1;i<=n;i++)b[i]=a[i];
        for(int u=1;u<=n;u++){
            if(a[u]){
                b[u]--;
                for(int i=head[u];i;i=nx[i]){
                    b[to[i]]++;
                }
            }
        }
        cnt=0;
        for(int i=1;i<=n;i++){
            a[i]=b[i];
            if(b[i])cnt++;
        }
        if(!cnt){cout<<k<<endl;return ;}
    }
    queue<int>q;
    vector<ll>dp(n+1);
    for(int i=1;i<=n;i++){
        if(in[i]==0)q.push(i);
    }
    int last=0;
    while(q.size()){
        int u=q.front();q.pop();
        dp[u]+=a[u];dp[u]%=MOD;
        last=u;
        for(int i=head[u];i;i=nx[i]){
            int v=to[i];
            dp[v]+=dp[u];
            dp[v]%=MOD;
            in[v]--;
            if(in[v]==0)q.push(v);
        }
    }
    cout<<(dp[last]+n)%MOD<<endl;
    return ;
}

int main(){
    int t=read();
    while(t--)solve();
    return 0;
}

4.[SCOI2009]粉刷匠
首先每个木板是相互独立的,不难首先想到设\(dp_{i,j}\)表示前i个木板用了j次操作的最大值
接下来就是如何处理每个木板,也就是如何求出转移方程
如果我们知道\(f_{i,j}(第i块木板用j次操作的最大值)\),那么状态很好表示
\(dp_{i,j}=max_{0\leq k \leq j}(dp_{i-1,k}+f_{i,j-k})\)
考虑如何求得f数组
当前的二维数组无法表示清楚第i块木板,不妨增加一维
\(f_{i,j,k}\)表示第i个木板的前j个格子用了k次操作的最大值
\(f_{i,j,k}=max_{0\leq t <j}(f_{i,t,k-1}+w_{i,t+1,j})\)
\(w_{i,l,r}\)表示第i个木板,粉刷一次在l到r的格子上的最大值
那么就能得到f数组
那么
\(dp_{i,j}=max_{0\leq k \leq j}(dp_{i-1,k}+f_{i,m,j-k})\)

总结:
实际是一个分组背包问题——将每条木板看成一个物品组,则物品组中每个物品\(i\)的费用\(c_i\)和价值\(w_i\)分别为操作次数\(k\)\(f_{i,m,k}\)。所求为从每组中最多选一件,求解将哪些物品装入背包可使这些物品的费 用总和不超过背包容量T,且价值(粉刷正确的格子数)总和最大。

点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#include<set>
#define ll long long 
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=2e6+101;
const int MOD=998244353;
const ll inf=2147483647;
const double eps=1e-12;

ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}
int n,m,t;
int dp[51][2501],f[51][51][2501],w[51][51];
int main(){
    n=read();m=read();t=read();
    for(int i=1;i<=n;i++){
        string a;cin>>a;
        for(int j=1;j<=m;j++){
            int cnt0=0,cnt1=0;
            for(int k=j;k<=m;k++){
                if(a[k-1]=='0')cnt0++;
                else cnt1++;
                w[j][k]=max(cnt0,cnt1);
            }
        }
        for(int j=1;j<=m;j++)for(int k=1;k<=m;k++){
            for(int t=0;t<j;t++){
                f[i][j][k]=max(f[i][j][k],f[i][t][k-1]+w[t+1][j]);
            }
        }
    }
    for(int i=1;i<=n;i++)for(int j=0;j<=t;j++){
        for(int t=0;t<=j;t++){
            dp[i][j]=max(dp[i][j],dp[i-1][t]+f[i][m][j-t]);
        }
    }
    cout<<dp[n][t]<<endl;
    return 0;
}

5.F - Tournament
题意:有\(2^n\)个人,最开始1和2比赛,3和4比赛····,随后1和2号的赢者与3和4号的赢者比赛,
以此类推进行n轮比赛,最后只剩一人。\(c_{i,j}\)表示i号赢了j场比赛的奖金,求比赛结束后,每个人的奖金之和最大。

题解:
可以把比赛过程图当成一颗满二叉树
\(dp_{i,j}\)表示i连续赢了j场比赛,i所在子树的最大值(不包含当前i节点的奖金)

假设求\(dp_{1,2}\),转移来自另一个非1所在的子树的结果
\(dp_{1,2}=dp_{1,1}+max(dp_{3,1}+c[3][1],dp_{4,1}+c[4][1])\)
所以
\(dp_{i,j}=dp_{i,j-1}+max_v (dp_{v,j-1}+c[v][j-1])\)
一个一个枚举肯定不行,可以预处理出最大值来转移

点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#include<set>
#define ll long long 
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=2e5+101;
const int MOD=998244353;
const ll inf=2147383647;
const double eps=1e-12;

ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}
ll n,c[maxn][18],dp[maxn][18];
ll getmaxx(int l,int r,int dep){
    ll maxx=0;
    for(int i=l;i<=r;i++)maxx=max(maxx,dp[i][dep]+c[i][dep]);
    return maxx;
}
void solve(int k,int l,int r,int dep){
    if(l==r)return;
    int mid=(l+r)>>1;
    solve(k<<1,l,mid,dep-1);solve(k<<1|1,mid+1,r,dep-1);
    ll maxl=getmaxx(l,mid,dep-1),maxr=getmaxx(mid+1,r,dep-1);
    for(int i=l;i<=mid;i++)dp[i][dep]=dp[i][dep-1]+maxr;
    for(int i=mid+1;i<=r;i++)dp[i][dep]=dp[i][dep-1]+maxl;
    return  ;
}
int main(){
    n=read();
    for(int i=1;i<=(1<<n);i++)for(int j=1;j<=n;j++)c[i][j]=read();
    solve(1,1,(1<<n),n);
    ll ans=0;
    for(int i=1;i<=(1<<n);i++)ans=max(ans,dp[i][n]+c[i][n]);
    printf("%lld\n",ans);
    return 0;
}

6.E. Swap and Maximum Block
题意:
有一个长度为\(2^n\)的数组a。接着有q个操作,每个操作给定一个k。交换\(a_i,a_{i+2^k}\)所有对之后(从前往后交换,如果某个位置已经和前面交换,则跳过),输出得到的数组的最大子段和。
每次询问后,改变当前的数组。
题解:
这道题有几个性质需要发现:

  1. 考虑交换的性质,不难发现对于k来说,把数组每\(2^{k+1}\)分成一段,这一段的前\(2^k\)平移到后\(2^k\),后\(2^k\)平移到前\(2^k\)
  2. 交换偶数次相同的k,数组不发生改变
  3. 交换是可交换的。例如先k=1后k=2和先k=2和后k=1是一样的

由性质2和3,我们可以把当前操作表示成2进制数,从低到高第i位表示操作k=i是否交换
由性质1,我们把数组区间按照二叉树分段,像线段树那样是一棵满二叉树。操作k,则相当于对树中倒数第k+1层的所有节点的两个儿子进行交换(从0开始)。
同时,线段树也可以很好求出最大子段和。不妨线段树和状态表示进行结合,我们把每个状态的答案求出,最后询问\(O(1)\)给出结果。
我们设\(tr[t][st]\)表示线段树上t节点所表示区间的操作状态为st时子段的信息(包含前缀最大值,后缀最大值,区间总和,当前区间状态位st的答案)
对于t节点所表示区间可以翻转和不翻转两种可能进行枚举
st'表示子区间的状态,k表示t节点交换左右儿子的操作位置
不翻转:\(tr[t][st']=tr[t<<1][st']+tr[t<<1|1][st']\)
翻转 :\(tr[t][st'|1<<k]=tr[t<<1][st']+tr[t<<1|1][st']\)
这里为什么左右儿子状态st'是一样的?因为操作是对整体数组操作的,左儿子进行的操作,右儿子一定进行了
这里的重载+

struct node{
    ll lmx,rmx,sum,ans;
    node(ll x,ll y,ll z,ll t){
        lmx=x;rmx=y;sum=z;ans=t;
    }
    node operator +(node x){
        node y=node(0,0,0,0);
        y.lmx=max(lmx,sum+x.lmx);
        y.rmx=max(x.rmx,x.sum+rmx);
        y.sum=sum+x.sum;
        y.ans=max(rmx+x.lmx,max(ans,x.ans));
        return y;
    }
};

这样按着线段树递归求解就行

但是st的状态为\(2^n\)数组开不下且超时
但是子区间的状态就减半了,枚举也减半了
比如倒数第二层有\(2^{n-1}\)个点,每个点有\(2\)种可能,到数第二层的状态为\(2^n\)
倒数第三层有\(2^{n-2}\)个点,每个点有\(2^2\)种可能,到数第二层的状态为\(2^n\)
以此类推,有n层,那么时间和空间复杂度为\(O(n*2^n)\)

点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#include<set>
#define ll long long 
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=5e5+101;
const int MOD=998244353;
const ll inf=2147383647;
const double eps=1e-12;

ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}
ll n,q,a[maxn];
struct node{
    ll lmx,rmx,sum,ans;
    node(ll x,ll y,ll z,ll t){
        lmx=x;rmx=y;sum=z;ans=t;
    }
    node operator +(node x){
        node y=node(0,0,0,0);
        y.lmx=max(lmx,sum+x.lmx);
        y.rmx=max(x.rmx,x.sum+rmx);
        y.sum=sum+x.sum;
        y.ans=max(rmx+x.lmx,max(ans,x.ans));
        return y;
    }
};
vector<node>tr[maxn<<3];
void build(int k,int l,int r){
    if(l==r){
        tr[k].pb(node(max(0ll,a[l]),max(0ll,a[l]),a[l],max(0ll,a[l])));
        return ;
    }
    int mid=(l+r)>>1;
    build(k<<1,l,mid);build(k<<1|1,mid+1,r);
    int lr=(k<<1),rc=(k<<1|1);
    //当前位为0
    for(int i=0;i<tr[lr].size();i++){
        tr[k].pb(tr[lr][i]+tr[rc][i]);
    }
    //当前位为1
    for(int i=0;i<tr[lr].size();i++){
        tr[k].pb(tr[rc][i]+tr[lr][i]);
    }
    return ;
}

int main(){
    n=read();
    for(int i=1;i<=(1<<n);i++)a[i]=read();
    q=read();int now=0;
    build(1,1,(1<<n));
    while(q--){
        int x=read();
        now^=(1<<x);
        printf("%lld\n",tr[1][now].ans);
    }
    return 0;
}

7.E. Cross Swapping
题解

点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#include<set>
#define ll long long 
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=2e6+101;
const int MOD=998244353;
const ll inf=2147383647;
const double eps=1e-12;

ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}

int n,a[1001][1001],f[maxn];
int find(int x){
    if(f[x]==x)return x;
    return f[x]=find(f[x]);
}
int same(int x,int y){
    return find(f[x])==find(f[y]);
}
void merge(int x,int y){
    int xx=find(x),yy=find(y);
    f[xx]=yy;
    return ;
}
int vis[maxn];
void solve(){
    n=read();
    for(int i=1;i<=2*n;i++)f[i]=i,vis[i]=0;
    for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)a[i][j]=read();
    for(int i=1;i<=n;i++){
        for(int j=i+1;j<=n;j++){
            if(a[i][j]>a[j][i]){
                if(same(i,j))continue;//如果之前的点连过边了,就不再连了,因为之前的点字典序小
                //i-->j+n,i+n-->j
                //我们假设j+n和j是换的,通过这样连边,可以使得i不换,i+n不换
                //相当于小于等于n的点连向大与n的点(也就是让大于n当祖先),这样这个联通块中小于等于n的点的祖先都是大于n的,他们就是不选
                //如果大于n的点连向小于等于n的点,与上面情况相反
                merge(i,j+n);
                merge(i+n,j);
                //j>i
                //所以要不然最后find(j)==j
                //就是存在k>j,使得find(j)==k+n,但是一定有find(k)==k

            }
            else if(a[i][j]<a[j][i]){
                if(same(i,j+n))continue;
                merge(i,j);
                merge(i+n,j+n);
            }
        }
    }
    map<int,int>mm;
    for(int i=1;i<=n;i++){
        if(find(i)==i)mm[i]=1;
        //肯定会有find(i)==i
        //因为肯定会存在一些集合的祖先是i,那么这个集合中小于等于n的点都是换的
    }
    for(int k=1;k<=n;k++){
        if(mm[find(k)]==0)continue;
        //交换k行k列
        for(int i=1;i<=n;i++)swap(a[i][k],a[k][i]);
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++)printf("%d ",a[i][j]);puts("");
    }
    return ;
}
int main(){
    int t=read();
    while(t--)solve();
    return 0;
}

8.Z-Game on grid
题意:Alice先手,Bob后手,问Alice是否有必赢、必平、必输的三种可能。
题解:
有一个性质是关键,Alice只会走到\((i+j)%2==0\)的点上,Bob与之相反
那么我们从后往前推,假设推Alice必赢的可能
假如一个点\((i,j)\),是Alice在的位置,他可以走到\((i+1,j)和(i,j+1)\)两个Bob可能的所在点
那么他肯定会走其中一个能够必胜的点(或者2个点都不能必胜,走哪个都行)
相反,那么Bob肯定会走2个点必输的点
那么我们设\(dp_{i,j}=0/1\),0表示走到\((i,j)\)必输,1表示必赢
1.\((i+j)%2==1, dp_{i,j}=min(dp_{i+1,j},dp_{i,j+1})\)
2.\((i+j)%2==0,dp_{i,j}=max(dp_{i+1,j},dp_{i,j+1})\)
如果当前点是’A’,那么\(dp_{i,j}\)直接为1,如果是‘B’,\(dp_{i,j}\)直接为0,‘.’情况不变(按照上述转移方程转移)

求必平,必输可能与上述类似

点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#include<set>
#define ll long long 
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=2e6+101;
const int MOD=998244353;
const ll inf=2147383647;
const double eps=1e-12;

ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}
int n,m,bk[501][501],dp[501][501];
int get_dp(int now){
    memset(dp,0,sizeof(dp));
    if(bk[n][m]==now)dp[n][m]=1;
    else dp[n][m]=0;
    for(int j=m;j>=1;j--){
        for(int i=n;i>=1;i--){
            if(i==n && j==m)continue;
            if(i==n)dp[i][j]=dp[i][j+1];
            else if(j==m)dp[i][j]=dp[i+1][j];
            else if((i+j)&1)dp[i][j]=min(dp[i+1][j],dp[i][j+1]);
            else dp[i][j]=max(dp[i+1][j],dp[i][j+1]);
            //平局
            if(now==0 && bk[i][j]!=now)dp[i][j]=0;
            //A&B
            if(bk[i][j]==now && now>0)dp[i][j]=1;
            else if(now && bk[i][j]!=now && bk[i][j]!=0)dp[i][j]=0;
        }
    }
    return dp[1][1];
}
void solve(){
    n=read();m=read();
    for(int i=1;i<=n;i++){
        string a;cin>>a;
        for(int j=1;j<=m;j++){
            bk[i][j]=0;
            if(a[j-1]=='A')bk[i][j]=1;
            else if(a[j-1]=='B')bk[i][j]=2;
        }
    }
    if(get_dp(1))printf("yes ");
    else printf("no ");
    if(get_dp(0))printf("yes ");
    else printf("no ");
    if(get_dp(2))puts("yes");
    else puts("no");
    return;
}

int main(){ 
    int t=read();
    while(t--)solve();
    return 0;
}

9.D. Empty Graph
题意:给定一长度为n的数组,你可以进行k次操作任意改变数组的一个值,用该数组构建一张无向稠密图,对于任意[l, r]两点之间的路径长度为数组a上[l, r]范围内的最小值。求操作后该图上两个点之间最短路的最大值。
题解:
可以先从性质出发:最短路只有两种取得的方式,一种是从u直接走到v(也就是u和v相邻)。还有一种是u先走到权值最小的点,然后再走到v去,那么路径长度是数组上最小权值的两倍。
如何操作能构造出最长的最短路呢?考虑贪心
\(ans=min( minn*2 , MAX( min(a[i],a[i+1]) ) )\)
2种情况分类讨论

  1. 将k小数变为1e9,使得minn最大
  2. 将k-1小数变为1e9,使得MAX( min(a[i],a[i+1]) )最大

为什么要分为2种情况,因为第一种情况不一定是最优的,第一种情况我们期望ans=2*minn,但是ans可能等于MAX( min(a[i],a[i+1]) )

点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#include<set>
#define ll long long 
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=3e6+101;
const int MOD=1e9+7;
const int inf=1e9;
const double eps=1e-12;

ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}   
struct wzq{
    int val,id;
}a[maxn],b[maxn];
void solve(){
    int n=read(),k=read();
    for(int i=1;i<=n;i++)a[i].val=read(),a[i].id=i,b[i]=a[i];
    int ans=0;
    //ans=min(minn*2, MAX( min(a[i],a[i+1]) )  )

    //将k小数变为1e9,使得minn最大
    {
        sort(a+1,a+n+1,[](wzq i,wzq j){
            return i.val<j.val;
        });
        for(int i=1;i<=k;i++)a[i].val=inf;
        int minn=a[k+1].val;
        if(k+1>n)minn=inf;
        sort(a+1,a+n+1,[](wzq i,wzq j){
            return i.id<j.id;
        });
        for(int i=1;i<n;i++){
            ans=max(ans,min(a[i].val,a[i+1].val));
        }
        ans=min(ans,2*minn);
        //期望ans=2*minn,但是ans可能等于MAX( min(a[i],a[i+1]) )
    }
    //将k-1小数变为1e9,使得MAX( min(a[i],a[i+1]) )最大
    {
        sort(b+1,b+n+1,[](wzq i,wzq j){
            return i.val<j.val;
        });
        if(k-1>0)ans=max(ans,min(b[k].val*2,inf));
        else ans=max(ans,min(b[n].val,b[k].val*2));
        //注意k=1的时候单独考虑
    }
    cout<<ans<<endl;
    return ;
}

int main(){ 
    int t=read();
    while(t--)solve();
    return 0;
}

10.E2. LCM Sum (hard version)
题意:t组数据,每次给定\([l,r]\),求出当前范围内满足条件的三元组\((i,j,k),i<j<k\)的个数。条件是:\(lcm(i,j,k)\geq i+j+k\)
这道题分为easy和hard版本,easy版本 \(1\leq t\leq 5\) , hard版本 \(t\leq 10^5\),所有版本\(1\leq l \leq r\leq 2*10^5\)
题解:
对于这种有限制条件的计数题,一定要尝试反着考虑;或者当满足条件的个数很接近所有可能的个数的时候考虑反着想

反着的情况就是统计\(lcm(i,j,k) < i+j+k <3k\)的个数:
那么就两种可能

  1. \(lcm(i,j,k)=k, \:\:i+j+k<k(一定满足)\)
  2. \(lcm(i,j,k)=2k, i+j+k<2k \:\: =>i+j<k(不一定满足要考虑)\)

那么easy的做法就是
\([l,r]\)枚举k

  1. 对于第一种情况我们就枚举k的因子,然后求出\((i,j)\)的个数 \((i<j \:\: 且\:\: i,j\in k的因子)\)
    但是第一种情况不需要枚举,设k的因子为cnt,那么\((i,j)\)个数=\(C_{cnt}^2\)
  2. 对于第一种情况我们就枚举2k的因子,然后求出第一种情况没计算的\((i,j)\)的个数 \((i<j \:\: 且 \:\: i+j< k \:\: 且\:\: i,j\in 2k的因子)\),这种情况得枚举\((i,j)\)

虽然因子个数总数接近\(nlogn\),但是不是每个数的因子个数是\(logn\)
这就导致第二种情况枚举(i,j)的总时间复杂度接近\(O(t*n\sqrt n)\)会超时的
但是通过打表发现,第二种情况很少,只有形如\((3t,4t,6t)\:\:\: (6t,10t,15t) , t\in Z\) 这样的数,才满足\(lcm(i,j,k) = 2*k\)
那么我们单独累计就行
那么easy做法就如下

点击查看代码
vector<vector<int> >fac(400000+1);
void solve() {
    int L=read(),R=read(); 
    ll len=R-L+1;
    ll ans=len*(len-1)*(len-2)/6;
    for (int k=L;k<=R;k++) {
        ll cnt=0;
        for(auto x:fac[k])if(L<=x && x<k)cnt++;
        ans-=cnt*(cnt-1)/2;
        if(k%6==0 && k/2>=L)ans--;
        if(k%15==0 && k*2/5>=L)ans--;
    }
    printf("%lld\n",ans);
    return ;
}

int main(){
	//nlogn 求约数 
	for (int i = 1;i<=2e5;i++) {
        for (int j=i;j<= 2e5;j+=i)fac[j].push_back(i);
    }
	int t=read();
	while(t--)solve();
    return 0;
}

对于hard来说,\(t=10^5\)
我们离线做,因为\(i,j,k\in[l,r],i<j<k\)
\(f[i]\)\((i,j,k)\)的个数
假设当前k=8,约数有1,2,4,8
那么增加的贡献就是f[1]+=2,f[2]+=1,f[4]+=0,f[8]+=0
区间\([l,r]\)的答案就是就是\(f[l]+····+f[r-2]\)
单点修改,区间查询,用树状数组维护即可

点击查看代码
#include <bits/stdc++.h>
#define ll long long 
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=2e5+101;
const int MOD=998244353;
const ll inf=2147383647;
const double eps=1e-12;

ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}
int n=maxn;
struct Tree{
	ll f[maxn];
	int lowbit(int x){return x&(-x);}
	void add(int pos,ll val){
		for(int i=pos;i<=n;i+=lowbit(i))f[i]+=val;
	} 
	ll query(int pos){
		ll ans=0;
		for(int i=pos;i;i-=lowbit(i))ans+=f[i];
		return ans;
	}
	ll range_sum(int l,int r){
		return query(r)-query(l-1);	
	} 
}T;

struct info{
	ll l,r,ans;
	int id;
}q[maxn];
vector<vector<int> >fac(2e5+1);
int main(){
	//nlogn 求约数 
	for (int i = 1;i<=2e5;i++) {
        for (int j=i;j<=2e5;j+=i)fac[j].push_back(i);
    }
	int t=read();
	for(int i=1;i<=t;i++){
		q[i].l=read(),q[i].r=read();q[i].id=i;
		ll len=q[i].r-q[i].l+1;
		q[i].ans=len*(len-1)*(len-2)/6ll; 
		/*
			去掉3 4 6和6 10 15的情况
			统计2*L <= 6倍数 <=R  个数=R/6-2*(L-1)/6=R/6-(L-1)/3 
			统计5*L/2 <=15倍数 <=R 个数=R/15-5*(L-1)/2/15=R/15-(L-1)/6
		*/
		q[i].ans-=max(0ll,q[i].r/6-(q[i].l-1)/3);
		q[i].ans-=max(0ll,q[i].r/15-(q[i].l-1)/6);
	}
	sort(q+1,q+t+1,[](info i,info j){
		return i.r<j.r;
	});
	int l=0;
	for(int i=1;i<=t;i++){
		while(l<q[i].r){
			l++;
			int sz=fac[l].size()-2,len=sz;
			for(int j=0;j<len;j++){
				T.add(fac[l][j],sz);
				sz--;
			}
		}
		q[i].ans-=T.range_sum(q[i].l,q[i].r);
	}
	sort(q+1,q+t+1,[](info i,info j){
		return i.id<j.id;
	});
	for(int i=1;i<=t;i++)printf("%lld\n",q[i].ans); 
    return 0;
}
posted @ 2022-07-29 20:58  I_N_V  阅读(21)  评论(0编辑  收藏  举报