NOI 2008 题解

假面舞会

(传送门)

题意

有K类面具,第i类只能看见属于第i+1类的编号的面具,现给出一些可以看见的面具的信息,问最多和最少有多少面具。

分析

边权设为1 / -1,一共分下面几类情况:

第1类(单点入=出),对答案没有影响

第2类(自环),环上的点数一定是答案的倍数

第3类(两条链环),两个链的长度的差一定是答案的倍数

所以答案就是2、3的最大公约数(有2、3存在的情况下)

代码

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

const int MAXN=100000*2+5;
const int MAXE=MAXN*10;

int head[MAXN],cnt=0;
struct Edge
{
    int to,w,next;
} edges[MAXE];
void addEdge(int u,int v,int w)
{
    edges[cnt]=(Edge){v,w,head[u]};
    head[u]=cnt++;
}

int gcd(int x,int y)
{
    int ys;
    while(y)
    {
        ys=x%y;
        x=y;
        y=ys;
    }
    return x;
}

bool vis[MAXN];
int d[MAXN];
int n,m,ans,an,tmax,tmin;

void dfs(int u)
{
    vis[u]=1;
    for(int i=head[u];i!=-1;i=edges[i].next)
    {
        int v=edges[i].to,w=edges[i].w;
        if(!vis[v])
        {
            d[v]=d[u]+w;
            dfs(v);
        }
        else ans=gcd(ans,abs(d[u]+w-d[v]));
    }
}

void tree(int u)
{
    vis[u]=1;
    tmax=max(tmax,d[u]);
    tmin=min(tmin,d[u]);
    for(int i=head[u];i!=-1;i=edges[i].next)
    {
        int v=edges[i].to,w=edges[i].w;
        if(!vis[v])
        {
            d[v]=d[u]+w;
            tree(v);
        }
    }
}

int main()
{
    memset(head,-1,sizeof(head));
    cin>>n>>m;
    while(m--)
    {
        int u,v;
        scanf("%d%d",&u,&v);
        addEdge(u,v,1);
        addEdge(v,u,-1);
    }
    for(int i=1;i<=n;i++)
        if(!vis[i]) dfs(i);
    if(ans)
    {//有环
        for(an=3;an<ans&&ans%an;an++);
    }
    else
    {//无环
        memset(vis,0,sizeof vis);
        for(int i=1;i<=n;i++)
            if(!vis[i])
            {
                tmax=tmin=d[i]=0;
                tree(i);
                ans+=tmax-tmin+1;
            }
        an=3;
    }
    if(ans<3) ans=an=-1;
    cout<<ans<<" "<<an<<endl;
    return 0;
}


设计路线

(传送门)

题意

确定若干条规划路线,将其中的公路全部改建为铁路。 定义每条规划路线为一个长度大于1 的城市序列,每个城市在该序列中最多出现一次,序列中相邻的城市之间由公路直接相连(待改建为铁路)。任意两条路线不能有公共部分,当然在一般情况下是不可能将所有的公路修建为铁路的,因此从有些城市出发去往首都依然需要通过乘坐长途汽车,而长途汽车只往返于公路连接的相邻的城市之间,因此从某个城市出发可能需要不断地换乘长途汽车和火车才能到达首都。 一个城市的“不便利值”为从它出发到首都需要乘坐的长途汽车的次数,交通系统的“不便利值”为所有城市的不便利值的最大值,首都的“不便利值”为0。如何确定规划路线修建铁路使交通系统的“不便利值”最小,以及有多少种不同的规划路线的选择方案使得“不便利值”达到最小。输出mod Q 后的值(一条规划路线翻转依然认为是等价的,方案不同当且仅当其中一个方案中存在一条规划路线不属于另一个方案)

分析

画图可以知不便利值不会超过log(n),所以第一问可以枚举,与第二问一起解决。

第二问,发现每一个节点,只可能与其儿子连0-2条边,于是用dp[i][j][k]表示i为根的子树,i与儿子连k条边,不便利值为j时的方案种数,可以通过枚举每一个儿子是否连边,利用乘法原理来得到值。

f1si连边的方案数,f1=dp[son][j][0]+dp[son][j][1]

f2si不连边的方案数,f2=dp[son][j-1][0]+dp[son][j-1][1]+f[son][j-1][2].

转移:

dp[i][j][2]=dp[i][j][2]*f2+dp[i][j][1]*f1

dp[i][j][1]=dp[i][j][1]*f2+dp[i][j][0]*f1

dp[i][j][0]=dp[i][j][0]*f2

代码

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

const int MAXN=1e5+5;
vector<int> edges[MAXN];
long long dp[MAXN][2][3];//试验为j<2
int n,m,MOD;

int vis[MAXN];

long long cal(long long x)
{
	if(x%MOD) return x%MOD;
    if(x) return MOD;
    return 0;
}

void dfs1(int u)
{
	vis[u]=1;
	for(int i=0;i<edges[u].size();i++)
	{
		int v=edges[u][i];
		if(!vis[v]) dfs1(v);
	}
}

void dfs2(int u,int fa,int cnt)
{
    dp[u][cnt][0]=1;
    dp[u][cnt][1]=0;
    dp[u][cnt][2]=0;
    for(int i=0;i<edges[u].size();i++)
    {
        int v=edges[u][i];
        if(v==fa) continue;
        dfs2(v,u,cnt);
        long long f1=dp[v][cnt][0]+dp[v][cnt][1];
        long long f2=(cnt) ? dp[v][cnt-1][0]+dp[v][cnt-1][1]+dp[v][cnt-1][2] : 0;
        dp[u][cnt][2]=cal(dp[u][cnt][2]*f2+dp[u][cnt][1]*f1);
        dp[u][cnt][1]=cal(dp[u][cnt][1]*f2+dp[u][cnt][0]*f1);
        dp[u][cnt][0]=cal(dp[u][cnt][0]*f2);
    }
}

int main()
{
	cin>>n>>m>>MOD;
	for(int i=1;i<=m;i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		edges[x].push_back(y);
		edges[y].push_back(x);
	}

	dfs1(1);
	for(int i=1;i<=n;i++)
		if(!vis[i])
		{
			cout<<-1<<endl<<-1<<endl;
			return 0;
		}

	for(int cnt=0;;cnt++)
	{
		dfs2(1,-1,cnt);
		if(dp[1][cnt][0]+dp[1][cnt][1]+dp[1][cnt][2])
        {
            printf("%d\n%d\n",cnt,(dp[1][cnt][0]+dp[1][cnt][1]+dp[1][cnt][2])%MOD);
            return 0;
        }
	}
	return 0;
}


志愿者招募

(传送门)

题意

n天每天需要ai个志愿者,有m种志愿者每种从第xi天干到第yi天花费为ci,问最小花费

分析

看神犇的题解学如何建图(传送门),然后就是裸的费用流了

代码

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

#define S 0
#define T n+1
const int MAXN=1000+5;
const int MAXE=MAXN*40;
const int INF=0x3f3f3f3f;

struct Edge
{
    int x,y,next,cost,op,d;
} edges[MAXE];
int head[MAXN],tot,pre[MAXE];
int n,m,ans;
int a[MAXN],dis[MAXN];

void addEdge(int x,int y,int c,int d)
{
    edges[++tot]=(Edge){x,y,head[x],c,tot+1,d};
    head[x]=tot;
    edges[++tot]=(Edge){y,x,head[y],-c,tot-1,0};
    head[y]=tot;
}

bool spfa()
{
    queue<int> Q;
    bool vis[MAXN];
    memset(vis,0,sizeof(vis));
    memset(dis,0x3f,sizeof(dis));
    Q.push(S),vis[S]=0,dis[S]=0,pre[S]=-1;
    while(!Q.empty())
    {
        int u=Q.front();  Q.pop(),vis[u]=0;
        for(int i=head[u];i;i=edges[i].next)
        if(dis[edges[i].y]>dis[u]+edges[i].cost && edges[i].d>0)
        {
            dis[edges[i].y]=dis[u]+edges[i].cost;
            pre[edges[i].y]=i;
            if(!vis[edges[i].y])
            {
                vis[edges[i].y]=1;
                Q.push(edges[i].y);
            }
        }
    }
    return (dis[T]<INF);
}

void find()
{
    int flow=INF;
    for(int i=pre[T];i!=-1;i=pre[edges[i].x])
        flow=min(flow,edges[i].d);
    for(int i=pre[T];i!=-1;i=pre[edges[i].x])
    {
        edges[i].d-=flow;
        edges[edges[i].op].d+=flow;
        ans+=flow*edges[i].cost;
    }
}

int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    for(int i=1;i<=m;i++)
    {
        int x,y,z;
        scanf("%d%d%d",&x,&y,&z);
        addEdge(x,y+1,z,INF);
    }
    for(int i=1;i<=n+1;i++)
    {
        int tmp=a[i]-a[i-1];
        if(tmp>0) addEdge(S,i,0,tmp);
        else addEdge(i,T,0,-tmp);
        if(i>1) addEdge(i,i-1,0,INF);
    }

    while(spfa()) find();
    cout<<ans<<endl;
    return 0;
}


奥运物流

(传送门)

题意

物流基站 1...N ,每个站 i 仅有一个后继基站 Si,可以有多个前驱基站。基站 i 中需要继续运输的物资都将被运往后继基站 Si。编号为 1 的从任何物流基站都可将物资运往控制基站,物流基站称为控制基站,控制基站也有后继基站。高可靠性与低成本是主要设计目,设物流基站 i 有 w 个前驱基站 P1, P2, Pw,即这些基站以 i 为后继基站,则基站 i 的可靠性R(i)=Ci+k*∑(j=1 to w)R(Pj), Ci 和 k 都是常实数且恒为正,且有 k 小于 1。更改m个基站的后继基站,使得控制基站的可靠性 R(1)尽量大。

分析

贪心:若是改变后继,变为1一定最优。因为每个点的Ci都会被加到R1中,离1越远,乘k(0.3<=k<1)的越多,变得越少,所以变成1的儿子一定最优。存在环,环上的点用方程列出来len(环长度)个未知量,len个方程,找出通项。每一次找一个指向1使当前最优的点使其指向1,找m次。因为当前最优不一定全局最优,所以贪心只有70分。

Tree_Dp:环上点可以解R1的总公式来求环上的点对R1的影响。R1=Σ(Ci*K^d[i])/(1-k^len)(d[i]i1的长度)环长度确定时,每个点的贡献值随之确定。可以枚举环长进行Tree_DP,(忽略1到其后继的那条边)

定义dp[i][j][d]表示以i为根的子树,变j个点,到根的距离为d时的R1最大值。

dp[i][j][d]=max{Σ maxf[son][j1][d+1],f[son][j1][1]))} 

定义g[u][c][d] = max{f[u][c][d + 1], f[u][c][1]}简化方程,右边的两项其实就是对c的一个最大分配,可以用多重背包解决。
u=1时,方程右边只有第一项,并且其中的g[u][c][d]为f[u][c][1]。

代码

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

const int MAXN=60+5;
double dp[MAXN][MAXN][MAXN];
double g[MAXN][MAXN][MAXN];
double c[MAXN],f[MAXN],k[MAXN],ans;
int pre[MAXN],n,m;

void dfs(int u,int d)
{
	for(int i=2;i<=n;i++)
		if(pre[i]==u) dfs(i,d+1);
	for(int dd=min(2,d);dd<d+1;dd++)
    {//由于当前的d值可能被修改过(有可能它的每一个祖先被指向了1),所以从2开始枚举d,(d==1时不可能被修改过)
        memset(f,0,sizeof(f));
        for(int v=2;v<=n;v++)
        	if(pre[v]==u)
        		for(int j=m;j>=0;j--)
        			for(int k=j;k>=0;k--)
            			f[j]=max(f[j],f[k]+g[v][j-k][dd]);
        //对其各个子树进行多重背包
        for(int j=0;j<=m;j++)
            dp[u][j][dd]=f[j]+c[u]*k[dd];
    }
    if(d>1)//d>1,将其后继结点修改为1才有意义。
    {
        memset(f,0,sizeof(f));
        for(int v=2;v<=n;v++)
        	if(pre[v]==u)
        		for(int j=m;j>=0;j--)
        			for(int k=j;k>=0;k--)
            			f[j]=max(f[j],f[k]+g[v][j-k][1]);
        for(int j=1;j<=m;j++)
            dp[u][j][1]=f[j-1]+c[u]*k[1];
    }
    for(int i=0;i<=m;i++)
    for(int dd=0;dd<d;++dd)
        g[u][i][dd]=max(dp[u][i][dd+1],dp[u][i][1]);
    return;

}

int main()
{
	scanf("%d%d%lf",&n,&m,&k[1]);
	for(int i=2;i<=n;i++)
		k[i]=k[i-1]*k[1];
	for(int i=1;i<=n;i++)
		scanf("%d",&pre[i]);
	for(int i=1;i<=n;i++)
		scanf("%lf",&c[i]);
	for(int ths=pre[1],len=2;ths-1;ths=pre[ths],len++)
    {//尝试分别将每一个处在环上的结点的父结点修改成1,然后分别做一次动态规划。
    	memset(dp,0,sizeof(dp));
    	memset(g,0,sizeof(g));
    	
    	double now=0;
        int tmp=pre[ths];
    	pre[ths]=1;
        
    	for(int i=2;i<=n;i++)
    		if(pre[i]==1) dfs(i,1);
        memset(f,0,sizeof(f));

        for(int i=2;i<=n;i++)
        	if(pre[i]==1)
        		for(int j=m;j>=0;j--)
        			for(int k=j;k>=0;k--)
            			f[j]=max(f[j],f[k]+dp[i][j-k][1]);
        
        for(int i=0;i<m;i++)
        	now=max(now,f[i]);
        if(tmp==1) now=max(now,f[m]);//若ths的父结点本身就是1,说明剩余的子树可以用m次修改机会。
        ans=max(ans,(now+c[1])/(1-k[len]));
        pre[ths]=tmp;//将ths的父结点还原。  
    }
	printf("%lf\n",ans);
	return 0;
}

糖果雨

(传送门)

题意

维护一个线段集合支持以下操作:
1. add T L R D :在T秒时加入一条L至R的线段,运动方向为D(每秒向右移动D个单位),D为1或-1。
2. ask t L R: 询问t时刻有多少个线段与L~R这个区间相交。
3.del X: 删除某线段。
地图左界为0,右界为len。线段长度不超过len。每一秒线段们都在运动,当一条线段左端点碰到地图左右界时方向会改变。

分析

首先所有的点的出现时间和初始位置可以互相转化,t时刻出现在(l,r),等价于t-l*d时刻出现在0位置。那么将所有的点全部等价到0位置,就只剩下了2个变量:出现时刻t与区间长度q。以t为横坐标,q为纵坐标,就可以将云变成一个点,如果能知道每一次的询问的区间是多少,就可以转化为区间统计问题可以发现这是2个五边形,但是这样无法用算法来统计,于是补一下形,将q<0的部分补上,于是就变成了:2个随时间不断右移平行四边形了,这样也无法统计,于是需要将平行四边形变成矩形,具体方法是所有点坐标变为(x,y-k*x),其中k是斜率,这道题的2个平行四边形斜率分别为-11,所以只要变成(x,y+x)(x,y-x)就可以了。然后通过推导出询问的平行四边形的四个顶点坐标,变成矩形,用二维的树状数组动态统计即可。推导的过程需要自己动手,题目还有很多细节与特殊情况需要考虑。

代码

#include <bits/stdc++.h>
using namespace std;
 
const int maxn=2000+5,maxm=maxn*2,maxc=1000010;
int len,n,m,q;
int s[2][maxn][maxm],x[maxc],y[maxc];
 
inline int lowbit(int x)
{
    return x&(-x);
}
 
inline void add(int p,int x,int y,int v)
{
    for(++x,++y;x<maxn;x+=lowbit(x))
        for(int i=y;i<maxm;i+=lowbit(i))
            s[p][x][i]+=v;
}
 
inline int sum(int p,int x,int y)
{
    if(x<0 || y<0) return 0;
    int ans=0;
    if(++x>n) x=n+1;
    if(++y>m) y=m+1;   
    for(;x;x-=lowbit(x))
        for(int i=y;i;i-=lowbit(i))
            ans+=s[p][x][i];
    return ans;
}
 
inline void updata(int t,int c,int l,int r,int d)
{
    x[c]=(t-l*d+n)%n,y[c]=r-l;
    add(0,x[c],y[c]+x[c],1);
    add(1,x[c],y[c]-x[c]+n,1);
}
 
void del(int c)
{
    add(0,x[c],y[c]+x[c],-1);
    add(1,x[c],y[c]-x[c]+n,-1);
}
 
inline int area(int p,int x1,int y1,int x2,int y2)
{
    return sum(p,x2,y2)+sum(p,x1-1,y1-1)-sum(p,x1-1,y2)-sum(p,x2,y1-1);
}
 
inline int ask(int t,int l,int r)
{
    int d=(r==len);
    return area(0,t,l+t,t+r,m)+area(0,0,l+t-n,t+r-n-d,m)+area(1,n-r+t+d,l-t,n,m)+area(1,t-r,l-t+n,t-1,m);
}
 
int main()
{
    int Q;
    cin>>Q>>len;
    n=len<<1,m=len<<2;
    while(Q--)
    {
        int t,c,l,r,d,p;
        scanf("%d%d",&p,&t);
        if(p==1)
        {
            scanf("%d%d%d%d",&c,&l,&r,&d);
            updata(t,c,l,r,d);
        }
        else if(p==2)
        {
            scanf("%d%d",&l,&r);
            printf("%d\n",ask(t%n,l,r));
        }else
        {
            scanf("%d",&c);
            del(c);
        }
    }
    return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值