NOI 2009 题解

变换序列

(传送门)

题意

对于0N-1共N个整数,给一个距离序列D0…DN-1,定义一个变换序列T0…TN-1使得每个i,Ti的环上距离等于Di。一个合法的变换序列应是0…N-1的一个排列,任务是要求出字典序最小的那个变换序列。

分析

一看就是二分图。左边为0~N-1的位置,右边为0~N-1的数字。如果第x个位置可以填y,则连一条线。题目所求为最大匹配。许多人用了稀奇古怪的方法来保证字典序,其实不用。不断寻找增广路,找到了就变,所以直接使用匈牙利就可以保证字典序最小。

代码

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

const int maxn=100000+5;
int n;
int mat[maxn];
bool vis[maxn];
vector<int> edges[maxn];

bool dfs(int v)
{

    for(int i=0;i<edges[v].size();i++) 
    {
      int e=edges[v][i];
        if(!vis[e])
      {
          vis[e]=1;
          if(mat[e]==-1 || dfs(mat[e]))
          {
              mat[e]=v;
              mat[v]=e;
              return 1;
          }
      }
  }
    return 0;
}

int main()
{
  	cin>>n;
    memset(mat,-1,sizeof(mat));

    for(int i=1;i<=n;i++)
    {
      int a;
        scanf("%d",&a);
        int x=i-a,y=i+a;
        if(x<=0) x+=n;
        if(y>n) y-=n;
        x+=n;
        y+=n;
        if(x>y) swap(x,y);
        edges[i].push_back(x);
        edges[i].push_back(y);
    }

    for(int i=n;i>=1;i--)
    {
        memset(vis,0,sizeof(vis));
        if(!dfs(i))
        {
          cout<<"No Answer";
          return 0;
        }
    }
  	for(int i=1;i<n;i++)
    	printf("%d ",mat[i]-n-1);
  printf("%d\n",mat[n]-n-1);
  return 0;
}

诗人小G

(传送门)

题意

有N个诗句需要被排版为若干行,顺序不能改变。一行内可以有若干个诗句,相邻诗句之间有一个空格。定义行标准长度L,每行的不协调度为|实际长度-L|P,整首诗的不协调度就是每行不协调度之和。任务是安排一种排版方案,使得整首诗的不协调度最小

分析

显然是动态规划问题,设前i个句子和为sum[i]

dp[i]表示以第i个为结尾的最小不协调度则有dp[i]=min(dp[j]+abs(sum[i]-sum[j]-l)^p);

写出DP方程有30分,加贪心50分,可证明状态转移具有决策单调性,可以加优化得到满分算法。如果在考场,不用费时费力证明,打个表就可以看出来。

代码

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

const int maxn=100000+5;
const long long INF=1e18;
//long double 保存大于long long的数
long double dp[maxn];
long double sum[maxn]; 
int q[maxn],ll[maxn],rr[maxn],head,tail;
int n,m,limit,p;

long double POW(long double a)
{
    if(a<0) a=-a;
    long double ans=1;
    for(int i=1;i<=p;i++) ans*=a;
    return ans;
}
long double cal(int i,int j)
{
    return dp[j]+POW(sum[i]-sum[j]+i-j-1-limit);
}

void update(int i)
{
    while(ll[tail]>i && cal(ll[tail],q[tail])>cal(ll[tail],i)) rr[tail-1]=rr[tail--];
    int l=ll[tail],r=rr[tail],pos=r+1;
    while(l<=r)
    {
        int mid=(l+r)>>1;
        if(cal(mid,q[tail])>cal(mid,i)) r=mid-1,pos=mid;
        else l=mid+1;
    }
    if(pos<=rr[tail])
    {
        ll[tail+1]=pos;
        rr[tail+1]=rr[tail];
        rr[tail]=pos-1;
        q[++tail]=i;
    }
}

void solve()
{
    head=tail=1;
    ll[head]=1,rr[head]=n;
    for(int i=1;i<=n;i++)
    {
        if(i>rr[head]) head++;
        dp[i]=cal(i,q[head]);
        update(i);
    }
}

int main()
{
    int T;
    cin>>T;
    while(T--)
    {
        scanf("%d%d%d",&n,&limit,&p);
        char s[32];
        for(int i=1;i<=n;i++)
        {
            scanf("%s",s);
            sum[i]=sum[i-1]+strlen(s);
        }

        solve();

        if(dp[n]>INF) printf("Too hard to arrange\n");
        else printf("%lld\n",(long long)dp[n]);
        printf("--------------------\n");
    }
    return 0;
}

二叉查找树

(传送门)

题意

一棵Treap,每个节点有个互不相同的数据值和权值,一个访问频度。一个节点的访问代价为它的访问频度乘以它在树中的深度,整棵树的访问代价定义为所有节点的访问代价之和。节点的权值可以修改为任意实数,每修改一个节点的权值的代价为K。任务是修改一些节点的权值,使得整棵树的访问代价与修改代价之和最小。

分析

先把权值离散化,按数据值排序
sum[i]为前i个节点频度和
dp[i][j][w]表示把节点[i,j]合并成一颗根节点权值不小于w的子树所需的访问代价与修改代价的最小和,答案为dp[1][n][1]
dp[i][j][w]=min{ dp[i][k-1][w]+dp[k+1][j][w]+sum[j]-sum[i-1]+K,dp[i][k-1][a[k].weight]+dp[k+1][j][a[k].weight]+sum[j]-sum[i-1](a[k].weight>=w) } (i<=k<=j)

代码

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

const int MAXN=80;
const int INF=0x3f3f3f3f;
struct node
{
    int key,weight,frequence;
} a[MAXN];
bool cmp_key(node aa,node bb) { return aa.key<bb.key; }
bool cmp_weight(node aa,node bb) { return aa.weight<bb.weight; };

int sum[MAXN];
int dp[MAXN][MAXN][MAXN];

int main()
{
    int n,ewai;
    cin>>n>>ewai;
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i].key);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i].weight);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i].frequence);
    sort(a+1,a+n+1,cmp_weight);
    for(int i=1;i<=n;i++)
        a[i].weight=i;
    sort(a+1,a+n+1,cmp_key);
    sum[0]=0;
    for(int i=1;i<=n;i++)
        sum[i]=sum[i-1]+a[i].frequence;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
        {
            if(a[i].weight>=j) dp[i][i][j]=a[i].frequence;
            else dp[i][i][j]=a[i].frequence+ewai;
        }
    for(int w=n;w>=1;w--)
        for(int t=1;t<n;t++)
            for(int i=1;i+t<=n;i++)
            {
                int j=i+t,s=INF;
                for(int k=i;k<=j;k++)
                {
                    s=min(s,dp[i][k-1][w]+dp[k+1][j][w]+sum[j]-sum[i-1]+ewai);
                    if(a[k].weight>=w)
                        s=min(s,dp[i][k-1][a[k].weight]+dp[k+1][j][a[k].weight]+sum[j]-sum[i-1]);
                }
                dp[i][j][w]=s;
            }
    printf("%d\n",dp[1][n][1]);
    return 0;
}

植物大战僵尸

(传送门)

题意

有一些植物,每个植物携带有一定的资源(可正可负),且有一个攻击范围,可以保护攻击位置上的另一个植物。有一群僵尸从右向左进攻植物,僵尸不能走到植物的攻击范围内。任务是求一个进攻方案,使得僵尸获得的资源尽可能多。

分析

建立图论模型,把植物当做点,携带的资源数目为顶点的权值。如果一个植物b在另一个植物a的攻击范围内,连有向边a-->b,表示a可以保护b。由于僵尸从右向左进攻,认为每个植物都被它右边相邻的植物保护,除最左边一列,对于每个植物a,向其右边植物连边b-->a表示b可以保护a。植物的攻击速度比僵尸快,所以当有的植物互相依赖(形成环),不可能被攻击到。在建图形成环的时候,只用最大流跑会把整个环都取到,所以,跑一半遍TopSort,把图中的环去掉。

代码

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

#define S 0
#define T (m*n+1)
const int MAXN=1000+5;
const int INF=0x3f3f3f3f;

int n,m,total_cost;
int score[MAXN];

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

bool vis[MAXN];
int out[MAXN];
void top_sort()
{
	queue<int> Q;
	while(!Q.empty())   Q.pop();
    for(int i=1;i<=m*n;i++)
		if(!out[i]) Q.push(i);
	while(!Q.empty())
	{
		int x=Q.front(); Q.pop();
		vis[x]=1;
		if(score[x]>0)
			total_cost+=score[x];
		for(int i=head[x];i;i=edges[i].next) 
            if(!edges[i].w && !--out[edges[i].v])
                Q.push(edges[i].v);
	}
}

bool bfs()
{
    memset(layer,-1,sizeof(layer));
    layer[S]=0;
    queue<int> Q;
    while(!Q.empty()) Q.pop();
    Q.push(S);
    while(!Q.empty())
    {
        int u=Q.front(); Q.pop();
        for(int i=head[u];i!=-1;i=edges[i].next)
        {
            int v=edges[i].v;
            if(layer[v]==-1 && edges[i].w)
            {
                layer[v]=layer[u]+1;
                Q.push(v);
            }
        }
    }
    return (layer[T]>0);
}

int dfs(int x,int flow)
{
    if(x==T) return flow;
    int a,ans=0;
    for(int i=head[x];i!=-1;i=edges[i].next)
    {
        int v=edges[i].v;
        if(layer[v]==layer[x]+1 && edges[i].w && (a=dfs(v,min(flow,edges[i].w) ) ) )
        {
            edges[i].w-=a;
            edges[i^1].w+=a;
            ans+=a;
            flow-=a;
            if(!flow) break;
        }
    }
    if(ans) return ans;
    layer[x]=-1;
    return 0;
}

int dinic()
{
    int max_flow=0;
    while(bfs())
       while(int k=dfs(0,INF))
            max_flow+=k;
    return max_flow;
}

int main()
{
	memset(head,-1,sizeof(head));
	cin>>m>>n;
	for(int i=1;i<=m*n;i++)
	{
		int num;
		scanf("%d%d",&score[i],&num);
		for(int j=1;j<=num;j++)
		{
			int x,y;
			scanf("%d%d",&x,&y);
			addEdge(x*n+y+1,i,INF);
            out[x*n+y+1]++;
		}
		if(i%n)
		{
			addEdge(i,i+1,INF);
			out[i]++;
		}
	}

	top_sort();

	for(int i=1;i<=m*n;i++)
        if(vis[i])
        {
            if(score[i]>0) addEdge(S,i,score[i]);
            else addEdge(i,T,-score[i]);
        }
	cout<<total_cost-dinic()<<endl;
	return 0;
}


管道取珠

(传送门)

题意

上下两个管道,管道内排列着两种颜色的珠。按照某种次序取出珠,可以形成一个输出序列。不同的取珠方法可以形成相同的输出序列,设某种输出序列的取珠方法数为a[i],任务是求出所有输出序列的∑a[i]2

分析

首先想到爆搜,只有30分,而且没有优化得到高分的头绪。见到有神犇用组合数学做了满分,膜拜。其实可以用DP来做

dp[i1][j1][i2][j2]可以表示为取珠方法X状态为(i1,j1),取珠方法Y状态为(i2,j2),X,Y的输出序列相同的有序对(X,Y)的数量。
由于X,Y同步,所以当i1,j1,i2都确定以后j2可以被表示为i1 + j1 - i2,状态表示可以减少一维。

代码

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

const int maxn=500+5;
const int MOD=1024523;
int n,m;
int dp[maxn][maxn][maxn];
char a[maxn],b[maxn];
inline void add(int &x,int y)
{
    x+=y;
    if(x>MOD)x-=MOD;
}
int main()
{
   	cin>>n>>m;
    scanf("%s",a+1);scanf("%s",b+1);
    reverse(a+1,a+n+1);
    reverse(b+1,b+m+1);
    dp[0][0][0]=1;
    for(int i=0;i<=n;i++)
        for(int j=0;j<=m;j++)
            for(int k=0;k<=n;k++)
            {
                int l=i+j-k,t=dp[i][j][k];
                if(l<0||l>m)continue;
                if(a[i+1]==a[k+1])add(dp[i+1][j][k+1],t);
                if(a[i+1]==b[l+1])add(dp[i+1][j][k],t);
                if(b[j+1]==a[k+1])add(dp[i][j+1][k+1],t);
                if(b[j+1]==b[l+1])add(dp[i][j+1][k],t);
            }
    printf("%d",dp[n][m][n]);
    return 0;
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值