NAIPC 2017 部分题解 (CDGH)

 

Contest Link

 

C. Stretching Streamers【dp思路+状态设计】

可以把gcd先预处理一下,然后就变成了Codeforces 888F,即如何在圆上$n$个点进行划分、使得划分线不相交。

根据$1$和$n$是否相连,分为两种情况。

当$1$和$n$相连时,考虑枚举最大的一个分界点$i$,要求$i+1...n$不会有边连向$1...i$。故可以划分成两个子问题,即划分$1...i$($1$与$i$可能相连)和划分$i+1...n$($i+1$与$n$可能相连)。

当$1$和$n$不相连时,考虑枚举最大的一个与$1$相连的点$i$。此时$i+1...n$必然有点连向$i$,故可以划分成两个子问题,即划分$1...i$($1$与$i$必须相连)和划分$i...n$($i$与$n$可能相连)。

于是用$dp[l][r][0/1]$表示,划分区间$[l,r]$、且$l,r$是否必须相连时的方案数。就有$dp[l][r][0]=\sum_{mid} dp[l][mid][0/1]\cdot dp[mid+1][r][0/1]$,$dp[l][r][1]=\sum_{mid} dp[l][mid][1]\cdot dp[mid][r][0/1]$。注意连边是存在限制的。

时间复杂度:$O(n^3)$

(下面是Codeforces 888F的代码)

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

typedef long long ll;
const int N=505;
const int mod=1000000007;

int n;
int a[N][N];

ll dp[N][N][2];

void dfs(int l,int r)
{
    if(dp[l][r][0]!=-1)
        return;
    
    dp[l][r][0]=dp[l][r][1]=0;
    for(int i=l;i<r;i++)
    {
        ll L,R;
        dfs(l,i),dfs(i,r),dfs(i+1,r);
        
        L=a[l][i]?dp[l][i][1]:0;
        R=dp[i][r][0]+(a[i][r]?dp[i][r][1]:0);
        dp[l][r][0]=(dp[l][r][0]+L*R)%mod;
        
        L=dp[l][i][0]+(a[l][i]?dp[l][i][1]:0);
        R=dp[i+1][r][0]+(a[i+1][r]?dp[i+1][r][1]:0);
        dp[l][r][1]=(dp[l][r][1]+L*R)%mod;
    }
}

int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            scanf("%d",&a[i][j]);
    
    memset(dp,-1,sizeof(dp));
    for(int i=1;i<=n;i++)
    {
        dp[i][i][0]=1,dp[i][i][1]=0;
        for(int j=1;j<i;j++)
            dp[i][j][0]=dp[i][j][1]=0;
    }
    
    dfs(1,n);
    
    ll ans=dp[1][n][0]+(a[1][n]?dp[1][n][1]:0);
    printf("%lld\n",ans%mod);
    return 0;
}
View Code

 

D. Heaps from Trees【性质+启发式合并】

首先考虑树是一条链的情况,就相当于求LIS,所以算法的下界至少为$O(n\cdot logn)$。

考虑有什么求LIS的办法。其中一个是线段树,显然不太好扩展到树上。另一种是二分+类似栈的结构。

利用二分求LIS是这个思路:我们用一个类似栈的数据结构维护 LIS=$i$的子串结尾 的最小值。那么我们就可以在栈中二分找到 最后一个小于当前值的值,而这个值的下标就是 以当前值为结尾的最大LIS长度 再减$1$。得到了以当前值为结尾的最大LIS长度后,我们尝试更新栈,即取$stack[current\_LIS]=min(current\_LIS, current\_val)$,或当$current\_val$比所有数都大时直接插入在栈的最后。

现在考虑树上是什么情况。对于一条链,我们可以用相同的方法进行维护;对于一个有多个儿子的节点,我们需要将儿子所在的栈进行合并。通过观察,可以发现将多个栈进行合并的结果,就是将栈中的所有元素拿出来整体排序。于是可以用multiset来维护栈,而合并多个multiset的时候采用启发式合并。由于将儿子的multiset拷贝到当前节点复杂度会爆炸,所以本质上是开一个set数组、每个点对应一个set的下标,从而避免拷贝。最终结果为合并到根节点的multiset大小。

时间复杂度:$O(n\cdot (logn)^2)$

#include <set>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;

const int N=200005;

int n;
int a[N],p[N];
vector<int> v[N];

int id[N];
multiset<int> s[N];

void dfs(int x)
{
    for(int y: v[x])
    {
        dfs(y);
        if(s[id[x]].size()<s[id[y]].size())
            id[x]=id[y];
    }
    if(!id[x])
        id[x]=x;
    
    for(int y: v[x])
        if(id[y]!=id[x])
            for(int tmp: s[id[y]])
                s[id[x]].insert(tmp);
    
    auto pos=s[id[x]].lower_bound(a[x]);
    if(pos!=s[id[x]].end())
        s[id[x]].erase(pos);
    s[id[x]].insert(a[x]);
}

int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d%d",&a[i],&p[i]);
        v[p[i]].emplace_back(i);
    }
    
    dfs(1);
    printf("%d\n",(int)s[id[1]].size());
    
    return 0;
}
View Code

 

E. Blazing New Trails【套路:带权二分】

 

G. Apple Market【套路:ST表优化建图】

最大流是逃不了的,但是建图需要设计一下。

裸的建图中,所有商店有$n\times n$个节点,每个节点向汇点连流量为$a[i][j]$的边;所有人共有$q$个节点,每个节点由源点流入流量$x_i$,并且连向$(down-up+1)\times (right-left+1)$个商店、流量为INF。那么此时共有$n\times n+q$个节点 和 $n\times n+n\times n\times q$条边。

现在我们考虑适当增加点数,从而减少$q$个节点所连的边数。

假如我们用类似二维线段树的结构,再构造$O(n^2)$级别的点,使得上层节点流入下层节点、线段树叶节点为单独的商店。由于我们在二维线段树上查询矩形需要$(logn)^2$个节点,所以建边能够优化为$n\times n+(log n)^2\times q$。不过这还不够。

由于我们不会对于图的结构进行任何修改,所以不妨考虑采用ST表结构。我们用$4$个参数表示一个节点$x,y,i,j$:$x,y$表示一个矩形区域左上角的坐标,$i,j$表示这个矩形在$x,y$方向的长度分别为$2^i,2^j$。一个节点$node[x][i][y][j]$可以连向$4$个节点:$node[x][i-1][y][j]$,$node[x+(1<<i-1)][i-1][y][j]$,$node[x][i][y][i-1]$,$node[x][i][y+(1<<i-1)][j-1]$,即分别是沿$x,y$方向对半切的子矩形。一维ST表覆盖区间只需要两个子区间,类似的,二维ST表覆盖一个矩形只需要$4$个子矩形。故此时总点数为$(n\cdot logn)^2+q$,总边数为$4(n\cdot logn)^2+4q$。

由于原图是二分图,故优化建图后跑网络流复杂度依然是$O(n\sqrt{n})$。

复杂度:$O(x\sqrt{x})$,其中$x=(n\cdot logn)^2+q$

#include <queue>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;

typedef long long ll;

//注意N是总点数
//有可能cap/flow超int 那么注意INF要重新设置 
const int N=200005;
const ll INF=1LL<<60;

struct edge
{
    ll cap;
    int to,rev;
    edge(int x,ll y,int z)
    {
        to=x,cap=y,rev=z;
    }
};

int s,t;
vector<edge> v[N];

inline void add_edge(int x,int y,ll cap)
{
    v[x].push_back(edge(y,cap,v[y].size()));
    v[y].push_back(edge(x,0,v[x].size()-1));
}

int level[N];
queue<int> Q;

void bfs()
{
    memset(level,-1,sizeof(level));
    level[s]=0;
    Q.push(s);
    
    while(!Q.empty())
    {
        int x=Q.front();
        Q.pop();
        
        for(int i=0;i<v[x].size();i++)
        {
            edge &e=v[x][i];
            if(e.cap>0 && level[e.to]<0)
            {
                level[e.to]=level[x]+1;
                Q.push(e.to);
            }
        }
    }
}

int iter[N];

ll dfs(int x,ll f)
{
    if(x==t)
        return f;
    for(int &i=iter[x];i<v[x].size();i++)
    {
        edge &e=v[x][i];
        if(e.cap>0 && level[e.to]>level[x])
        {
            ll d=dfs(e.to,min(f,e.cap));
            if(d>0)
            {
                e.cap-=d;
                v[e.to][e.rev].cap+=d;
                return d;
            }
        }
    }
    return 0;
}

ll max_flow()
{
    ll flow=0;
    while(1)
    {
        bfs();
        if(level[t]<0)
            break;
        
        memset(iter,0,sizeof(iter));
        
        ll f=0;
        while((f=dfs(s,INF))>0)
            flow+=f;
    }
    return flow;
}

int n,m,q;
int tot,id[100][6][100][6];

int main()
{
    scanf("%d%d%d",&n,&m,&q);
    
    for(int i=0;(1<<i)<=n;i++)
        for(int j=0;(1<<j)<=m;j++)
            for(int x=1;x<=n;x++)
                for(int y=1;y<=m;y++)
                {
                    id[x][i][y][j]=++tot;
                    
                    if(i==0 && j==0)
                        continue;
                    
                    if(i==0)
                    {
                        add_edge(tot,id[x][i][y][j-1],INF);
                        add_edge(tot,id[x][i][y+(1<<j-1)][j-1],INF);
                        continue;
                    }
                    
                    if(j==0)
                    {
                        add_edge(tot,id[x][i-1][y][j],INF);
                        add_edge(tot,id[x+(1<<i-1)][i-1][y][j],INF);
                        continue;
                    }
                    
                    add_edge(tot,id[x][i-1][y][j-1],INF);
                    add_edge(tot,id[x+(1<<i-1)][i-1][y][j-1],INF);
                    add_edge(tot,id[x][i-1][y+(1<<j-1)][j-1],INF);
                    add_edge(tot,id[x+(1<<i-1)][i-1][y+(1<<j-1)][j-1],INF);
                }
    
    s=++tot,t=++tot;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
        {
            int x;
            scanf("%d",&x);
            add_edge((i-1)*m+j,t,x);
        }
    
    while(q--)
    {
        int u,d,l,r,x;
        scanf("%d%d%d%d%d",&u,&d,&l,&r,&x);
        
        add_edge(s,++tot,x);
        
        int x_log=0,y_log=0;
        while((1<<x_log+1)<=d-u+1)
            x_log++;
        while((1<<y_log+1)<=r-l+1)
            y_log++;
        
        add_edge(tot,id[u][x_log][l][y_log],INF);
        add_edge(tot,id[d-(1<<x_log)+1][x_log][l][y_log],INF);
        add_edge(tot,id[u][x_log][r-(1<<y_log)+1][y_log],INF);
        add_edge(tot,id[d-(1<<x_log)+1][x_log][r-(1<<y_log)+1][y_log],INF);
    }
    
    printf("%lld\n",max_flow());
    return 0;
}
View Code

 

H. Maximum Color Clique(性质+dp状态设计

需要能看出一个性质:因为“任意环中至少存在一对相邻同色边”,所以图中必然存在一个点,其连接的所有边颜色相同。

利用反证法,假如不存在这样的一个点,我们从任意一点出发,每次走与上条边颜色不同的边(因为根据假设,每个点至少存在两种不同颜色的边),那么一直走下去一定能走到一个点,其与起点的边与第一条边(或路径上出现过的某个点)的颜色不同。

我们将这个点和与其相连的边从图中删去,那么剩下的图中仍然存在一个这样的点(因为“任意环中至少存在一条相邻同色边”的条件不会改变),一直这样删可以得到一个序列。我们可以用 每个点颜色全相同的出边颜色 来作为每个点的tag。我们在序列上任意选一个点集$S$,$S$中颜色为$c$的subset大小就是$S$中点tag为$c$的点的数量(不算$S$中下标最大的点)。

我们按照$S$中最后一个下标 从小到大的顺序进行dp。问题就变成了,共有$300$个颜色,每个颜色有$cnt_i$个节点,我们从中最多选择$x$个同颜色节点的选择方案数。可以考虑依次对于每个颜色均进行一次背包。假设当前枚举的节点颜色为$color$,那么枚举$1...color-1$中最多选择同颜色节点的数量为$pre\_mx$、再枚举在当前颜色中选取$cur\_sel$个点。此时能进行一次转移$new\_dp[max(pre\_mx,cur\_sel+1)]+=dp[pre\_mx]\cdot \begin{pmatrix}cnt[color]\\ cur\_sel\end{pmatrix}$。对于$cur\_sel+1$是因为下标最大 的点可以属于任何一个颜色的subset。

这样看起来要进行$4$个for循环嵌套,但是实际上$\sum_{color} cnt[color]$是不超过$n$的,所以其实是$O(n^3)$的一个dp。rls的dp思路比其余ac代码的要清晰很多。

时间复杂度:$O(n^3)$

#include <queue>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;

typedef long long ll;
const int N=305;
const int mod=1000000007;

ll C[N][N];

int n;
int a[N][N];

int cnt[N],deg[N][N];

inline void inc(int x,int c)
{
    if(++deg[x][c]==1)
        cnt[x]++;
}

inline void dec(int x,int c)
{
    if(--deg[x][c]==0)
        cnt[x]--;
}

int vis[N],ord[N],edge[N];

ll dp[N],tmp[N];

inline void add(ll &dest,ll dlt)
{
    dest=(dest+dlt)%mod;
}

int main()
{
    for(int i=0;i<N;i++)
        C[i][0]=C[i][i]=1;
    for(int i=1;i<N;i++)
        for(int j=1;j<i;j++)
            C[i][j]=(C[i-1][j-1]+C[i-1][j])%mod;
    
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
        {
            scanf("%d",&a[i][j]);
            if(i!=j && i<j)
                inc(i,a[i][j]),inc(j,a[i][j]);
        }
    
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            if(!vis[j] && cnt[j]==1)
            {
                vis[j]=1,ord[i]=j;
                
                for(int k=1;k<=n;k++)
                    if(!vis[k])
                        dec(k,a[j][k]),edge[i]=a[j][k];
                break;
            }
    
    for(int i=1;i<=300;i++)
        cnt[i]=0;
    dp[1]=1;
    
    ll ans=0;
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=i;j++)
            add(ans,j*dp[j]);
        
        memset(dp,0,sizeof(dp));
        dp[1]=1;
        
        cnt[edge[i]]++;
        
        for(int color=1;color<=300;color++)
        {
            memcpy(tmp,dp,sizeof(dp));
            memset(dp,0,sizeof(dp));
            
            for(int pre_mx=1;pre_mx<=i+1;pre_mx++)
                for(int cur_sel=0;cur_sel<=cnt[color];cur_sel++)
                    add(dp[max(pre_mx,cur_sel+1)],tmp[pre_mx]*C[cnt[color]][cur_sel]);
        }
    }
    printf("%lld\n",ans);
    return 0;
}
View Code

 

posted @ 2021-07-20 13:33  LiuRunky  阅读(165)  评论(0)    收藏  举报