2019 Multi-University Training Contest 3




A

题目地址:

题意

思路

代码




B Blow up the city

题目地址: http://acm.hdu.edu.cn/showproblem.php?pid=6604

题意:有 n 个城市,m 条有向边。定义特殊结点为:不存在出度的边。现在有 q 组询问,每一组询问给出两个结点 u 和 v,你可以破坏任意一个城市,使得如果 u 和 v 中至少有一个不能到达特殊结点,就算破坏成功。对于每组询问,回答有多少种成功的破坏方案。

思路:大致思路就是算出破坏 u 的方案数,然后算出破坏 v 的方案数,最后减去同时破坏两个的方案数。学了支配树之后就会做了。把图反向连边,然后新增一个结点向所有入度为 0 (反向连边后的图)连边,然后以新增节点为根结点构建支配树,那么支配树中某个结点的祖先就是成功的方案。所以计算每个结点的深度之后,最后答案就是 d[u] + d[v] - d[LCA(u, v)] 。

代码

#include <bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define REP(i,a,b) for(int i=(a);i<=(int)(b);i++)
#define REP_(i,a,b) for(int i=(a);i>=(b);i--)
#define pb push_back
using namespace std;
typedef long long LL;
typedef vector<int> VI;
typedef pair<int,int> P;
int read()
{
    int x=0,flag=1;
    char c=getchar();
    while((c>'9' || c<'0') && c!='-') c=getchar();
    if(c=='-') flag=0,c=getchar();
    while(c<='9' && c>='0') {x=(x<<3)+(x<<1)+c-'0';c=getchar();}
    return flag?x:-x;
}

struct dominant_tree
{
    #define maxn 100005
    VI a[maxn],b[maxn],c[maxn],t[maxn];
    int dfn[maxn],which[maxn],f[maxn],cnt,n,root;
    int sdom[maxn],idom[maxn],far[maxn],val[maxn];

    void dfs(int u,int fa)
    {
        dfn[u]=++cnt; which[cnt]=u; f[u]=fa;
        for(int v:a[u]) if(!dfn[v]) dfs(v,u);
    }

    int find(int x)
    {
        if(x==far[x]) return x;
        int t=find(far[x]);
        if(dfn[sdom[val[far[x]]]]<dfn[sdom[val[x]]]) val[x]=val[far[x]];
        return far[x]=t;
    }

    void tarjan()
    {
        REP_(i,cnt,2)
        {
            int u=which[i];
            for(int v:b[u]) if(dfn[v])
            {
                find(v);
                if(dfn[sdom[val[v]]]<dfn[sdom[u]]) sdom[u]=sdom[val[v]];
            }
            c[sdom[u]].pb(u);
            far[u]=f[u]; u=f[u];
            for(int v:c[u])
            {
                find(v);
                if(sdom[val[v]]==u) idom[v]=u;
                else idom[v]=val[v];
            }
        }
        REP(i,2,cnt)
        {
            int u=which[i];
            if(idom[u]!=sdom[u]) idom[u]=idom[idom[u]];
        }
    }

    void build(int n,int root,vector<P> &E)
    {
        this->n=n; this->root=root; cnt=0;
        REP(i,0,n) a[i].clear(),b[i].clear(),c[i].clear(),t[i].clear();
        REP(i,0,n) sdom[i]=val[i]=far[i]=i;
        REP(i,0,n) idom[i]=dfn[i]=f[i]=which[i]=0;
        for(P e:E)
        {
            int u=e.first,v=e.second;
            a[u].pb(v); b[v].pb(u);
        }
        dfs(root,0);
        tarjan();
        REP(i,1,n) if(i!=root) t[idom[i]].pb(i);
    }
}xx;

int du[maxn],d[maxn],siz[maxn],f[maxn],son[maxn];
int dfn[maxn],which[maxn],top[maxn],cnt;

void dfs1(int u,int fa,int depth)
{
    f[u]=fa; d[u]=depth; siz[u]=1; son[u]=0;
    for(int v:xx.t[u])
    {
        if(v==fa) continue;
        dfs1(v,u,depth+1);
        siz[u]+=siz[v];
        if(siz[v]>siz[son[u]]) son[u]=v;
    }
}

void dfs2(int u,int tf)
{
    top[u]=tf; dfn[u]=++cnt; which[cnt]=u;
    if(!son[u]) return;
    dfs2(son[u],tf);
    for(int v:xx.t[u])
    {
        if(v!=f[u] && v!=son[u]) dfs2(v,v);
    }
}

int LCA(int x,int y)
{
    while(top[x]!=top[y])
        d[top[x]]>d[top[y]]?(x=f[top[x]]):(y=f[top[y]]);
    return d[x]<d[y]?x:y;
}

int main()
{
    int T=read();
    while(T--)
    {
        int n=read(),m=read();
        REP(i,1,n) du[i]=0;
        cnt=0;
        vector<P> E;
        while(m--)
        {
            int u=read(),v=read();
            E.pb(P(v,u));
            du[u]++;
        }
        REP(i,1,n) if(!du[i]) E.pb(P(n+1,i));
        xx.build(n+1,n+1,E);
        dfs1(n+1,0,0);
        dfs2(n+1,n+1);
        int q=read();
        while(q--)
        {
            int a=read(),b=read();
            printf("%d\n",d[a]+d[b]-d[LCA(a,b)]);
        }
    }

    return 0;
}



C

题目地址:

题意

思路

代码




D Distribution of books

题目地址: http://acm.hdu.edu.cn/showproblem.php?pid=6606

题意:给出 n 本书,每一本书有一个值 a[i],现在要把前若干本书(可以是前 n 本或者更少,这个自己决定)分割成 k 段,每一段的值为这一段所有书的值之和,而所有段的最大值为分割的代价。求最小代价。

思路:显然要二分最小代价,然后对于某个代价 x,要求把书分成 k 段,每一段的和不超过 x。一开始我的做法是贪心,但是后来发现是错的,比如对于 -3 -3 2 -4 要分成两段,如果用贪心发现 -4 是不行的,但实际上可以。

考虑 dp,设 f(i) 表示前 i 本书最多可以分成多少段,那么只要存在 s i − s j ≤ x s_i-s_j\le x sisjx(其中 s 为 a 的前缀和)就可以有转移 f i = f j + 1 f_i=f_j+1 fi=fj+1 。上述条件可以转化为 s j ≥ s i − x s_j \ge s_i-x sjsix 。所以思路就很明显了:用一个类似权值线段树的东西维护,首先把所有的 s 离散化,然后每次算完 f[i] 之后更新对应 s[i] 的权值为 f[i];然后每次要计算 f[i] 时,找到满足 s j ≥ s i − x s_j \ge s_i-x sjsix 的区域的最大值,i 就从那里转移。复杂度 O ( n ( l o g n ) 2 ) O(n(logn)^2) O(n(logn)2)

代码

#include <bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define REP(i,a,b) for(int i=(a);i<=(int)(b);i++)
#define REP_(i,a,b) for(int i=(a);i>=(b);i--)
#define pb push_back
using namespace std;
typedef long long LL;
typedef vector<int> VI;
int read()
{
    int x=0,flag=1;
    char c=getchar();
    while((c>'9' || c<'0') && c!='-') c=getchar();
    if(c=='-') flag=0,c=getchar();
    while(c<='9' && c>='0') {x=(x<<3)+(x<<1)+c-'0';c=getchar();}
    return flag?x:-x;
}

template <class T>
struct segment_tree_max
{
    #define chl (k<<1)
    #define chr (k<<1|1)
    #define mid ((l+r)>>1)
    #define inf 2147483647
    T *t;
    int n;

    segment_tree_max(int n) {t=new T[n<<2](); this->n=n;}
    void del() {delete[] t;}

    void build(int k,int l,int r)
    {
        if(l==r) {t[k]=-1; return;}
        build(chl,l,mid); build(chr,mid+1,r);
        push_up(k);
    }
    void build() {build(1,0,n);}

    void push_up(int k) {t[k]=max(t[chl],t[chr]);}

    void update(int k,int l,int r,int id,T x)
    {
        if(l>r || id<l || id>r) return;
        if(l==r && l==id) {t[k]=x; return;}
        update(chl,l,mid,id,x); update(chr,mid+1,r,id,x);
        push_up(k);
    }
    void update(int id,T x) {update(1,0,n,id,x);}

    T query(int k,int l,int r,int ll,int rr)
    {
        if(l>rr || ll>r) return -inf;
        if(l>=ll && r<=rr) return t[k];
        return max(query(chl,l,mid,ll,rr),query(chr,mid+1,r,ll,rr));
    }
    T query(int ll,int rr) {return query(1,0,n,ll,rr);}
};


const int maxn=2e5+5;
int n,k,a[maxn],len,f[maxn];
LL s[maxn],b[maxn];
int ID(LL x) {return lower_bound(b+1,b+len+1,x)-b;}

bool can(LL x)
{
    segment_tree_max<int> t(len);
    t.build();
    t.update(ID(0),0);
    REP(i,1,n)
    {
        int q=t.query(ID(s[i]-x),len);
        if(q>=0) f[i]=q+1;
        else f[i]=-1;
        t.update(ID(s[i]),f[i]);
        if(f[i]>=k) {t.del(); return 1;}
    }
    return 0;
}

int main()
{
    int T=read();
    while(T--)
    {
        n=read(),k=read();
        REP(i,1,n) a[i]=read(),s[i]=s[i-1]+a[i],b[i]=s[i];
        b[n+1]=0; len=n+1;
        sort(b+1,b+len+1);
        len=unique(b+1,b+len+1)-b-1;

        LL l=-1e15+5,r=1e15+5;
        while(l<r-1)
        {
            if(can(mid)) r=mid;
            else l=mid;
        }
        printf("%lld\n",r);
    }

    return 0;
}



E

题目地址:

题意

思路

代码




F Fansblog

题目地址: http://acm.hdu.edu.cn/showproblem.php?pid=6608

题意:给出一个质数 P( 1 e 9 ≤ P ≤ 1 e 14 1e9\le P \le 1e14 1e9P1e14),找到最大的比 P 小的质数 Q,然后计算 Q !   m o d   P Q! \ mod \ P Q! mod P

思路:根据威尔逊定理,一个数是质数的等价条件是 ( P − 1 ) ! ≡ − 1 ( m o d   P ) (P-1)! \equiv -1 (mod \ P) (P1)!1(mod P) 。然后质数之间的间隔也不是很大,所以就从 P-1 开始往下枚举找到下一个质数,然后计算就行了。这里要注意 long long 会炸精度,所以要用龟速乘。然后判断素数可以用费马素性测试。

代码

#include <bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define REP(i,a,b) for(int i=(a);i<=(int)(b);i++)
#define REP_(i,a,b) for(int i=(a);i>=(b);i--)
#define pb push_back
using namespace std;
typedef long long LL;
typedef vector<int> VI;
int read()
{
    int x=0,flag=1;
    char c=getchar();
    while((c>'9' || c<'0') && c!='-') c=getchar();
    if(c=='-') flag=0,c=getchar();
    while(c<='9' && c>='0') {x=(x<<3)+(x<<1)+c-'0';c=getchar();}
    return flag?x:-x;
}

LL multi(LL x,LL y,LL M)
{
    LL ret=0;
    while(y)
    {
        if(y&1) ret=(ret+x)%M;
        x=(x<<1)%M;
        y>>=1;
    }
    return ret;
}

LL ksm(LL x,LL n,LL M)
{
    LL ret=1;
    while(n)
    {
        if(n&1) ret=multi(ret,x,M);
        x=multi(x,x,M);
        n>>=1;
    }
    return ret;
}

bool is_prime(LL x)
{
    REP(i,1,100)
    {
        if(ksm(i,x-1,x)!=1) return 0;
    }
    return 1;
}

int main()
{
    //freopen("input.txt","r",stdin);
    int T=read();
    LL P;
    while(T--)
    {
        cin>>P;
        LL Q=P-1;
        while(!is_prime(Q)) Q--;
        LL ans=P-1;
        for(LL x=P-1;x>Q;x--) ans=multi(ans,ksm(x,P-2,P),P);
        cout<<ans<<endl;
    }

    return 0;
}



G Find the answer

题目地址: http://acm.hdu.edu.cn/showproblem.php?pid=6609

题意:给出一个数组 a,对于每个 i,可以找出若干个小于 i 的下标,然后把这些下标对应的值变为 0 。问对于每个 i,至少变多少个为 0,才能使 a[1,…,i] 的和小于等于 m。

思路:一开始先求出 a 中每个数的排名,然后维护两个树状数组,一个维护前 i 个数的和(按照排名来维护),另一个维护排名中哪些排名已经被选中了。从前往后遍历 a,然后二分查找第一个树状数组,找到最大的一个 sum[x]<=m-a[i] 的 x,然后第二个树状数组的前 x 项和为 s,i - 1 - s 就是第 i 个数的答案。

复杂度 O ( n ( l o g n ) 2 ) O(n(logn)^2) O(n(logn)2) ,然而可以把两个树状数组合起来,就不用二分那一步,因为在搜索最大的 x 这一步可以直接边搜索边记录已经累加的和,可以把复杂度降到 O ( n l o g n ) O(nlogn) O(nlogn) ,不过原本的也过了,可能是树状数组常数很小的缘故吧。

代码

#include <bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define REP(i,a,b) for(int i=(a);i<=(int)(b);i++)
#define REP_(i,a,b) for(int i=(a);i>=(b);i--)
#define pb push_back
using namespace std;
typedef long long LL;
typedef vector<int> VI;
int read()
{
    int x=0,flag=1;
    char c=getchar();
    while((c>'9' || c<'0') && c!='-') c=getchar();
    if(c=='-') flag=0,c=getchar();
    while(c<='9' && c>='0') {x=(x<<3)+(x<<1)+c-'0';c=getchar();}
    return flag?x:-x;
}

// Binary Indexed Tree
template<class T>
struct BIT
{
    T *a,*c;
    int n;
    BIT(int n) {this->n=n; a=new T[n+5](); c=new T[n+5]();}

    int lowbit(int w) {return w&(-w);}
    T sum(int w) {T ret=0; while(w>0) ret+=c[w],w-=lowbit(w); return ret;}
    void update(int w,T x) {a[w]+=x; while(w<=n) c[w]+=x,w+=lowbit(w);}
    T sum(int l,int r) {return sum(r)-sum(l-1);}
};


const int maxn=2e5+5;
int a[maxn],w[maxn];
pair<int,int> b[maxn];

int main()
{
    //freopen("input.txt","r",stdin);
    int T=read();
    while(T--)
    {
        int n=read(),m=read();
        REP(i,1,n) a[i]=read(),b[i]=make_pair(a[i],i);
        sort(b+1,b+n+1);
        REP(i,1,n) w[b[i].second]=i;
        BIT<LL> ts(n);
        BIT<int> tt(n);
        REP(i,1,n)
        {
            int l=0,r=n+1;
            while(l<r-1)
            {
                int mid=(l+r)>>1;
                if(ts.sum(mid)<=m-a[i]) l=mid;
                else r=mid;
            }
            printf("%d ",i-1-tt.sum(l));
            ts.update(w[i],a[i]);
            tt.update(w[i],1);
        }
        puts("");
    }

    return 0;
}



H

题目地址:

题意

思路

代码




I K Subsequence

题目地址: http://acm.hdu.edu.cn/showproblem.php?pid=6611

题意:有一个长度为 n( 1 ≤ n ≤ 2000 1\le n\le 2000 1n2000) 的数组,每次操作可以从中选取一个单调不减的子序列,然后把子序列的和累加到答案中,然后把子序列从原数组中删去。这样的操作最多进行 k( 1 ≤ k ≤ 10 1\le k\le 10 1k10) 次。问最后答案的最大值。多组数据。

思路:一开始我的想法是,贪心地选取:每次操作都选择和最大的子序列,然后把它删去,这样选取 k 次。每次选取都可以用一个 O ( n 2 ) O(n^2) O(n2) 的 dp 算出来,然后复杂度也对。但是这样做是错的,比如:6 4 4 8 5 ,k=2 。如果用贪心的做法,第一次会选取 4 4 8 ,第二次会选取 6 ;而最优的解肯定是 6 84 4 5

后来看到很多博客说,求多个不相交的子序列,很容易想到网络流(黑人问号???表示之前从来没有这种思路)

不过网络流确实可以搞这种问题,在一定的条件下(这里是 ∀ i < j , a i ≤ a j \forall i<j,a_i\le a_j i<j,aiaj),对每个点拆点,然后拆开的两个点连一个容量为 1 的边,那么保证每个结点只被选择一次,然后在这题中可以把所有 a i ≤ a j a_i\le a_j aiaj 的 i 连向 j,容量为 1,这样保证可以选择多个单调不减的子序列,并且由于有之前的限制,子序列不会相交。然后 s 向所有结点连容量为 1 的边,t 也是。这样的模型就可以求多个不相交子序列了。

这里还要考虑 a[i] 这个权值,所以给拆点之间的连边加一个 -a[i] 的费用,其它边费用为 0,那么在流量为 k 的前提下的最小费用的负值,就是最终答案。不得不说这种建模方式真厉害。

然而传统的 SPFA 的最小费用流在这里被卡了,还要 Dijkstra 优化最短路的过程。原本每次 SPFA 是为了以费用为权值在还有容量的边中(这个好像叫残余网络?)求 s 到 t 的最短路,然后在这条最短路上增广。但是由于反向边图中存在负权边,一般的 Dijkstra 不适用,所以要仿照 Johnson 算法给每个结点增加一个势函数 H,然后原本 <u, v> 边的权值 w 就变成了 w+H(u)-H(v) 。这里势函数是把每次求出的 dis 累加,然后最短路增广时用的是势函数,这里我一直没想明白为什么,可能是因为我还不理解网络流,只知道怎么传统地用的缘故吧。(先放在这儿,以后再回来看)

代码

#include <bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define REP(i,a,b) for(int i=(a);i<=(int)(b);i++)
#define REP_(i,a,b) for(int i=(a);i>=(b);i--)
#define pb push_back
using namespace std;
typedef long long LL;
typedef vector<int> VI;
int read()
{
    int x=0,flag=1;
    char c=getchar();
    while((c>'9' || c<'0') && c!='-') c=getchar();
    if(c=='-') flag=0,c=getchar();
    while(c<='9' && c>='0') {x=(x<<3)+(x<<1)+c-'0';c=getchar();}
    return flag?x:-x;
}

const int maxn=2e4+5,inf=1e9;
struct edge {int to,cap,rev,cost;};
vector<edge> G[maxn];
int dis[maxn],book[maxn],prevv[maxn],preve[maxn],n,a[maxn],k,H[maxn];

void add_edge(int from,int to,int cap,int cost)
{
    G[from].push_back((edge){to,cap,(int)G[to].size(),cost});
    G[to].push_back((edge){from,0,(int)G[from].size()-1,-cost});
}

int min_cost_flow(int n,int s,int t,int flow)
{
    int ans=0;
    while(flow>0)
    {
        typedef pair<int,int> P;
        priority_queue<P,vector<P>,greater<P> > Q;
        fill(dis,dis+n+1,inf);
        dis[s]=0;
        Q.push(P(0,s));
        while(!Q.empty())
        {
            P p=Q.top(); Q.pop();
            int d=p.first,u=p.second;
            if(dis[u]<d) continue;
            REP(i,0,G[u].size()-1)
            {
                edge e=G[u][i];
                int v=e.to;
                if(e.cap>0 && dis[v]>d+e.cost+H[u]-H[v])
                {
                    dis[v]=d+e.cost+H[u]-H[v];
                    prevv[v]=u; preve[v]=i;
                    Q.push(P(dis[v],v));
                }
            }
        }
        if(dis[t]==inf) return -1;
        REP(i,1,n) H[i]+=dis[i];
        int d=flow;
        for(int v=t;v!=s;v=prevv[v]) d=min(d,G[prevv[v]][preve[v]].cap);
        flow-=d;
        ans+=d*H[t];
        for(int v=t;v!=s;v=prevv[v])
        {
            edge &e=G[prevv[v]][preve[v]];
            e.cap-=d;
            G[v][e.rev].cap+=d;
        }
    }
    return ans;
}

int main()
{
    //freopen("input.txt","r",stdin);
    int T=read();
    while(T--)
    {
        n=read(),k=read();
        REP(i,1,n) a[i]=read();
        int s=n*2+1,t=n*2+2;
        REP(i,1,t) G[i].clear(),H[i]=0;
        REP(i,1,n) add_edge(s,i,1,0),add_edge(i+n,t,1,0);
        REP(i,1,n) add_edge(i,i+n,1,-a[i]);
        REP(i,1,n) REP(j,i+1,n) if(a[i]<=a[j]) add_edge(i+n,j,1,0);
        printf("%d\n",-min_cost_flow(t,s,t,k));
    }

    return 0;
}



J

题目地址:

题意

思路

代码




K

题目地址:

题意

思路

代码




评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值