[NOIP2016 提高组]

[NOIP2016 提高组] 玩具谜题

题目背景

NOIP2016 提高组 D1T1

题目描述

小南有一套可爱的玩具小人, 它们各有不同的职业。

有一天, 这些玩具小人把小南的眼镜藏了起来。 小南发现玩具小人们围成了一个圈,它们有的面朝圈内,有的面朝圈外。如下图:

这时 singer 告诉小南一个谜題: “眼镜藏在我左数第 3 3 3 个玩具小人的右数第 1 1 1 个玩具小人的左数第 2 2 2 个玩具小人那里。 ”

小南发现, 这个谜题中玩具小人的朝向非常关键, 因为朝内和朝外的玩具小人的左右方向是相反的: 面朝圈内的玩具小人, 它的左边是顺时针方向, 右边是逆时针方向; 而面向圈外的玩具小人, 它的左边是逆时针方向, 右边是顺时针方向。

小南一边艰难地辨认着玩具小人, 一边数着:

singer 朝内, 左数第 3 3 3 个是 archer。

archer 朝外,右数第 1 1 1 个是 thinker 。

thinker 朝外, 左数第 2 2 2 个是 writer。

所以眼镜藏在 writer 这里!

虽然成功找回了眼镜, 但小南并没有放心。 如果下次有更多的玩具小人藏他的眼镜, 或是谜题的长度更长, 他可能就无法找到眼镜了。所以小南希望你写程序帮他解决类似的谜题。 这样的谜題具体可以描述为:

n n n 个玩具小人围成一圈, 已知它们的职业和朝向。现在第 1 1 1 个玩具小人告诉小南一个包含 m m m 条指令的谜題, 其中第 z z z 条指令形如“左数/右数第 s s s,个玩具小人”。 你需要输出依次数完这些指令后,到达的玩具小人的职业。

输入格式

输入的第一行包含两个正整数 n , m n,m n,m,表示玩具小人的个数和指令的条数。

接下来 n n n 行,每行包含一个整数和一个字符串,以逆时针为顺序给出每个玩具小人的朝向和职业。其中 0 0 0 表示朝向圈内, 1 1 1 表示朝向圈外。 保证不会出现其他的数。字符串长度不超过 10 10 10 且仅由小写字母构成,字符串不为空,并且字符串两两不同。整数和字符串之间用一个空格隔开。

接下来 m m m 行,其中第 i i i 行包含两个整数 a i , s i a_i,s_i ai,si,表示第 i i i 条指令。若 a i = 0 a_i=0 ai=0,表示向左数 s i s_i si 个人;若 a i = 1 a_i=1 ai=1,表示向右数 s i s_i si 个人。 保证 a i a_i ai 不会出现其他的数, 1 ≤ s i < n 1 \le s_i < n 1si<n

输出格式

输出一个字符串,表示从第一个读入的小人开始,依次数完 m m m 条指令后到达的小人的职业。

样例 #1

样例输入 #1

7 3
0 singer
0 reader
0 mengbier 
1 thinker
1 archer
0 writer
1 mogician 
0 3
1 1
0 2

样例输出 #1

writer

样例 #2

样例输入 #2

10 10
1 C
0 r
0 P
1 d
1 e
1 m
1 t
1 y
1 u
0 V
1 7
1 1
1 4
0 5
0 3
0 1
1 6
1 2
0 8
0 4

样例输出 #2

y

提示

【样例1说明】

这组数据就是【题目描述】 中提到的例子。

【子任务】

子任务会给出部分测试数据的特点。 如果你在解决题目中遇到了困难, 可以尝试只解决一部分测试数据。

每个测试点的数据规模及特点如下表:

其中一些简写的列意义如下:

  • 全朝内: 若为“√”, 表示该测试点保证所有的玩具小人都朝向圈内;

  • 全左数:若为“√”,表示该测试点保证所有的指令都向左数,即对任意的 1 ≤ z ≤ m , a i = 0 1\leq z\leq m, a_i=0 1zm,ai=0;

  • s = 1 s=1 s=1:若为“√”,表示该测试点保证所有的指令都只数 1 1 1 个,即对任意的 1 ≤ z ≤ m , s i = 1 1\leq z\leq m,s_i=1 1zm,si=1;

职业长度为 1 1 1:若为“√”,表示该测试点保证所有玩具小人的职业一定是一个长度为 1 1 1的字符串。

题解、代码

直接大模拟,没什么好说的

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

const int M = 100100;
int dir[M];
char name[M][15];

int n, m, now, di, cnt;

void in(){
	freopen("toy.in", "r", stdin);
	freopen("toy.out", "w", stdout);
}

int main()
{
//	in();
	
	scanf("%d%d", &n, &m);
	for(int i=0 ; i<n ; i++)
		scanf("%d%s", &dir[i], &name[i]);
	
	now = 0;
	for(int i=0 ; i<m ; i++)
	{
		scanf("%d%d", &di, &cnt);
		if(!di && !dir[now]) now=now+n-cnt;
		if(di && !dir[now]) now+=cnt;
		if(di && dir[now]) now=now+n-cnt;
		if(!di && dir[now]) now+=cnt;
		
		now+=n;
		now %= n;
 	}
	
	for(int i=0 ; name[now][i] ; i++)
		printf("%c", name[now][i]);
	
	return 0;
}

[NOIP2016 提高组] 天天爱跑步

题目描述

小c 同学认为跑步非常有趣,于是决定制作一款叫做《天天爱跑步》的游戏。《天天爱跑步》是一个养成类游戏,需要玩家每天按时上线,完成打卡任务。

这个游戏的地图可以看作一一棵包含 n n n 个结点和 n − 1 n-1 n1 条边的树,每条边连接两个结点,且任意两个结点存在一条路径互相可达。树上结点编号为从 1 1 1 n n n 的连续正整数。

现在有 m m m 个玩家,第 i i i 个玩家的起点为 s i s_i si,终点为 t i t_i ti。每天打卡任务开始时,所有玩家在第 0 0 0 秒同时从自己的起点出发,以每秒跑一条边的速度,不间断地沿着最短路径向着自己的终点跑去,跑到终点后该玩家就算完成了打卡任务。 (由于地图是一棵树,所以每个人的路径是唯一的)

小c 想知道游戏的活跃度,所以在每个结点上都放置了一个观察员。在结点 j j j 的观察员会选择在第 w j w_j wj 秒观察玩家,一个玩家能被这个观察员观察到当且仅当该玩家在第 w j w_j wj 秒也正好到达了结点 j j j小c 想知道每个观察员会观察到多少人?

注意:我们认为一个玩家到达自己的终点后该玩家就会结束游戏,他不能等待一 段时间后再被观察员观察到。 即对于把结点 j j j 作为终点的玩家:若他在第 w j w_j wj 秒前到达终点,则在结点 j j j 的观察员不能观察到该玩家;若他正好在第 w j w_j wj 秒到达终点,则在结点 j j j 的观察员可以观察到这个玩家。

输入格式

第一行有两个整数 n n n m m m。其中 n n n 代表树的结点数量, 同时也是观察员的数量, m m m 代表玩家的数量。

接下来 n − 1 n-1 n1 行每行两个整数 u u u v v v,表示结点 u u u 到结点 v v v 有一条边。

接下来一行 n n n 个整数,其中第 j j j 个整数为 w j w_j wj , 表示结点 j j j 出现观察员的时间。

接下来 m m m 行,每行两个整数 s i s_i si,和 t i t_i ti,表示一个玩家的起点和终点。

对于所有的数据,保证 1 ≤ s i , t i ≤ n , 0 ≤ w j ≤ n 1\leq s_i,t_i\leq n, 0\leq w_j\leq n 1si,tin,0wjn

输出格式

输出 1 1 1 n n n 个整数,第 j j j 个整数表示结点 j j j 的观察员可以观察到多少人。

样例 #1

样例输入 #1

6 3
2 3
1 2 
1 4 
4 5 
4 6 
0 2 5 1 2 3 
1 5 
1 3 
2 6

样例输出 #1

2 0 0 1 1 1

样例 #2

样例输入 #2

5 3 
1 2 
2 3 
2 4 
1 5 
0 1 0 3 0 
3 1 
1 4
5 5

样例输出 #2

1 2 1 0 1

提示

【样例1说明】

对于 1 1 1 号点, w i = 0 w_i=0 wi=0,故只有起点为 1 1 1 号点的玩家才会被观察到,所以玩家 1 1 1 和玩家 2 2 2 被观察到,共有 2 2 2 人被观察到。

对于 2 2 2 号点,没有玩家在第 2 2 2 秒时在此结点,共 0 0 0 人被观察到。

对于 3 3 3 号点,没有玩家在第 5 5 5 秒时在此结点,共 0 0 0 人被观察到。

对于 4 4 4 号点,玩家 1 1 1 被观察到,共 1 1 1 人被观察到。

对于 5 5 5 号点,玩家 1 1 1 被观察到,共 1 1 1 人被观察到。

对于 6 6 6 号点,玩家 3 3 3 被观察到,共 1 1 1 人被观察到。

【子任务】

每个测试点的数据规模及特点如下表所示。
提示: 数据范围的个位上的数字可以帮助判断是哪一种数据类型。

【提示】

如果你的程序需要用到较大的栈空间 (这通常意味着需要较深层数的递归), 请务必仔细阅读选手目录下的文本文档 running/stack.txt, 以了解在最终评测时栈空间的限制与在当前工作环境下调整栈空间限制的方法。

在最终评测时,调用栈占用的空间大小不会有单独的限制,但在我们的工作环境中默认会有 1 MiB 1 \text{MiB} 1MiB 的限制。 这可能会引起函数调用层数较多时, 程序发生栈溢出崩溃。

我们可以使用一些方法修改调用栈的大小限制。 例如, 在终端中输入下列命令
ulimit -s 1048576

此命令的意义是,将调用栈的大小限制修改为 1 GiB 1 \text{GiB} 1GiB

例如,在选手目录建立如下 sample.cpp 或 sample.pas

将上述源代码编译为可执行文件 sample 后,可以在终端中运行如下命令运行该程序

./sample

如果在没有使用命令“ ulimit -s 1048576”的情况下运行该程序, sample 会因为栈溢出而崩溃; 如果使用了上述命令后运行该程序,该程序则不会崩溃。

特别地, 当你打开多个终端时, 它们并不会共享该命令, 你需要分别对它们运行该命令。

请注意, 调用栈占用的空间会计入总空间占用中, 和程序其他部分占用的内存共同受到内存限制。

题解

这道题十分复杂,所以转载一篇来自洛谷的好题解,作者是greenlcat
在这里插入图片描述在这里插入图片描述
在这里插入图片描述

int tot1, tot2, h1[SIZE], h2[SIZE];
void add1(int x, int y)
{
	e1[++tot1].to=y;
	e1[tot1].next=h1[x];
	h1[x]=tot1;
}

void add2(int x, int y)
{
	e2[++tot2].to=y;
	e2[tot2].next=h2[x];
	h2[x]=tot2;
}

int b1[SIZE*2], b2[SIZE*2], js[SIZE], dist[SIZE], s[SIZE], t[SIZE], ans[SIZE];

void dfs2(int x)
{
	int t1=b1[w[x]+deep[x]], t2=b2[w[x]-deep[x]+SIZE];      //递归前先读桶里的数值,t1是上行桶里的值,t2是下行桶的值
	for(int i=h[x]; i; i=E[i].next)                         //递归子树
	{
		int y=E[i].to;
		if(y==fa[x][0]) continue;
		dfs2(y);
	}
	b1[deep[x]]+=js[x];                                     //上行过程中,当前点作为路径起点产生贡献,入桶
	for(int i=h1[x]; i; i=e1[i].next)                       //下行过程中,当前点作为路径终点产生贡献,入桶
	{
		int y=e1[i].to;
		b2[dist[y]-deep[t[y]]+SIZE]++;
	}
	ans[x]+=b1[w[x]+deep[x]]-t1+b2[w[x]-deep[x]+SIZE]-t2;   //计算上、下行桶内差值,累加到ans[x]里面
	for(int i=h2[x]; i; i=e2[i].next)                       //回溯前清除以此结点为LCA的起点和终点在桶内产生的贡献,它们已经无效了
	{
		int y=e2[i].to;
		b1[deep[s[y]]]--;                                   //清除起点产生的贡献
		b2[dist[y]-deep[t[y]]+SIZE]--;                      //清除终点产生的贡献
	}
}

int main()
{
重复部分跳过
文末提供完整代码
	for(int i=1; i<=m; i++)                                 //读入m条询问
	{
		scanf("%d%d", &s[i], &t[i]);
		int lca=get_lca(s[i], t[i]);                        //求LCA
		dist[i]=deep[s[i]]+deep[t[i]]-2*deep[lca]];         //计算路径长度
		js[s[i]]++;                                         //统计以s[i]为起点路径的条数,便于统计上行过程中该结点产生的贡献
		add1(t[i], i);                                      //第i条路径加入到以t[i]为终点的路径集合中
		add2(lca, i);                                       //把每条路径归到对应的LCA集合中
		if(deep[lca]+w[lca]==deep[s[i]]) ans[lca]--;        //见下面的解释
	}
	dfs2(1);                                                //dfs吧!
	for(int i=1; i<=n; i++) printf("%d ", ans[i]); 
	return 0;
}
一些重要补充

上述代码中有一行未加解释if(deep[lca]+w[lca]==deep[s[i]]) ans[lca]--;
考虑路径是这样的,如图:


这个图可能不太好懂,意思是:

如果路径起点或终点刚好为LCA且LCA处是可观察到运动员的,那么我们在上行统计过程中和下行统计过程中都会对该LCA产生贡献,这样就重复计数一次!

好在这种情况很容易发现,我们提前预测到,对相应的结点进行ans[x]--即可。

此外,在使用第二个桶时,下标是w[x]-deep[x]会成为负数,所以使用第二个桶时,下标统一+SIZE,向右平移一段区间,防止下溢。

4. 结束
我不知道自己说清楚没有,但愿大家不要拍砖头!下面是完整代码

#include<bits/stdc++.h>
using namespace std;
const int SIZE=300000;
int n, m, tot, h[SIZE], deep[SIZE], fa[SIZE][20], w[SIZE];
struct edge
{
	int to, next;
}E[SIZE*2], e1[SIZE*2], e2[SIZE*2];

void add(int x, int y)
{
	E[++tot].to=y;
	E[tot].next=h[x];
	h[x]=tot;
}

int tot1, tot2, h1[SIZE], h2[SIZE];
void add1(int x, int y)
{
	e1[++tot1].to=y;
	e1[tot1].next=h1[x];
	h1[x]=tot1;
}
void add2(int x, int y)
{
	e2[++tot2].to=y;
	e2[tot2].next=h2[x];
	h2[x]=tot2;
}

void dfs1(int x)
{
	for(int i=1; (1<<i)<=deep[x]; i++)
		fa[x][i]=fa[fa[x][i-1]][i-1];
	for(int i=h[x]; i; i=E[i].next)
	{
		int y=E[i].to;
		if(y==fa[x][0])	continue;
		fa[y][0]=x;
		deep[y]=deep[x]+1;
		dfs1(y);
	}
}

int get_lca(int x, int y)
{
	if(x==y) return x;
	if(deep[x]<deep[y]) swap(x, y);
	int t=log(deep[x]-deep[y])/log(2);
	for(int i=t; i>=0; i--)
	{
		if(deep[fa[x][i]]>=deep[y])
			x=fa[x][i];
		if(x==y)
			return x;
	}
	t=log(deep[x])/log(2);
	for(int i=t; i>=0; i--)
	{
		if(fa[x][i]!=fa[y][i])
			x=fa[x][i], y=fa[y][i];
	}
	return fa[x][0];
}

int b1[SIZE*2], b2[SIZE*2], js[SIZE], dist[SIZE], s[SIZE], t[SIZE], l[SIZE], ans[SIZE];
void dfs2(int x)
{
	int t1=b1[w[x]+deep[x]], t2=b2[w[x]-deep[x]+SIZE];
	for(int i=h[x]; i; i=E[i].next)
	{
		int y=E[i].to;
		if(y==fa[x][0]) continue;
		dfs2(y);
	}
	b1[deep[x]]+=js[x];
	for(int i=h1[x]; i; i=e1[i].next)
	{
		int y=e1[i].to;
		b2[dist[y]-deep[t[y]]+SIZE]++;
	}
	ans[x]+=b1[w[x]+deep[x]]-t1+b2[w[x]-deep[x]+SIZE]-t2;
	for(int i=h2[x]; i; i=e2[i].next)
	{
		int y=e2[i].to;
		b1[deep[s[y]]]--;
		b2[dist[y]-deep[t[y]]+SIZE]--;
	}
}

int main()
{
	scanf("%d%d", &n, &m);
	for(int i=1; i<n; i++)
	{
		int u, v;
		scanf("%d%d", &u, &v);
		add(u, v);
		add(v, u);
	}
	deep[1]=1;
	fa[1][0]=1;
	dfs1(1);
	for(int i=1; i<=n; i++) scanf("%d", &w[i]);
	for(int i=1; i<=m; i++)
	{
		scanf("%d%d", &s[i], &t[i]);
		int lca=get_lca(s[i], t[i]);
		dist[i]=deep[s[i]]+deep[t[i]]-2*deep[lca];
		js[s[i]]++;
		add1(t[i], i);
		add2(lca, i);
		if(deep[lca]+w[lca]==deep[s[i]]) ans[lca]--;
	}
	dfs2(1);
	for(int i=1; i<=n; i++) printf("%d ", ans[i]);
	return 0;
}

再次感谢原作者!

[NOIP2016 提高组] 换教室

题目描述

对于刚上大学的牛牛来说,他面临的第一个问题是如何根据实际情况申请合适的课程。

在可以选择的课程中,有 2 n 2n 2n 节课程安排在 n n n 个时间段上。在第 i i i 1 ≤ i ≤ n 1 \leq i \leq n 1in)个时间段上,两节内容相同的课程同时在不同的地点进行,其中,牛牛预先被安排在教室 c i c_i ci 上课,而另一节课程在教室 d i d_i di 进行。

在不提交任何申请的情况下,学生们需要按时间段的顺序依次完成所有的 n n n 节安排好的课程。如果学生想更换第 i i i 节课程的教室,则需要提出申请。若申请通过,学生就可以在第 i i i 个时间段去教室 d i d_i di 上课,否则仍然在教室 c i c_i ci 上课。

由于更换教室的需求太多,申请不一定能获得通过。通过计算,牛牛发现申请更换第 i i i 节课程的教室时,申请被通过的概率是一个已知的实数 k i k_i ki,并且对于不同课程的申请,被通过的概率是互相独立的。

学校规定,所有的申请只能在学期开始前一次性提交,并且每个人只能选择至多 m m m 节课程进行申请。这意味着牛牛必须一次性决定是否申请更换每节课的教室,而不能根据某些课程的申请结果来决定其他课程是否申请;牛牛可以申请自己最希望更换教室的 m m m 门课程,也可以不用完这 m m m 个申请的机会,甚至可以一门课程都不申请。

因为不同的课程可能会被安排在不同的教室进行,所以牛牛需要利用课间时间从一间教室赶到另一间教室。

牛牛所在的大学有 v v v 个教室,有 e e e 条道路。每条道路连接两间教室,并且是可以双向通行的。由于道路的长度和拥堵程度不同,通过不同的道路耗费的体力可能会有所不同。 当第 i i i 1 ≤ i ≤ n − 1 1 \leq i \leq n-1 1in1)节课结束后,牛牛就会从这节课的教室出发,选择一条耗费体力最少的路径前往下一节课的教室。

现在牛牛想知道,申请哪几门课程可以使他因在教室间移动耗费的体力值的总和的期望值最小,请你帮他求出这个最小值。

输入格式

第一行四个整数 n , m , v , e n,m,v,e n,m,v,e n n n 表示这个学期内的时间段的数量; m m m 表示牛牛最多可以申请更换多少节课程的教室; v v v 表示牛牛学校里教室的数量; e e e表示牛牛的学校里道路的数量。

第二行 n n n 个正整数,第 i i i 1 ≤ i ≤ n 1 \leq i \leq n 1in)个正整数表示 c i c_i ci,即第 i i i 个时间段牛牛被安排上课的教室;保证 1 ≤ c i ≤ v 1 \le c_i \le v 1civ

第三行 n n n 个正整数,第 i i i 1 ≤ i ≤ n 1 \leq i \leq n 1in)个正整数表示 d i d_i di,即第 i i i 个时间段另一间上同样课程的教室;保证 1 ≤ d i ≤ v 1 \le d_i \le v 1div

第四行 n n n 个实数,第 i i i 1 ≤ i ≤ n 1 \leq i \leq n 1in)个实数表示 k i k_i ki,即牛牛申请在第 i i i 个时间段更换教室获得通过的概率。保证 0 ≤ k i ≤ 1 0 \le k_i \le 1 0ki1

接下来 e e e 行,每行三个正整数 a j , b j , w j a_j, b_j, w_j aj,bj,wj,表示有一条双向道路连接教室 a j , b j a_j, b_j aj,bj,通过这条道路需要耗费的体力值是 w j w_j wj;保证 1 ≤ a j , b j ≤ v 1 \le a_j, b_j \le v 1aj,bjv 1 ≤ w j ≤ 100 1 \le w_j \le 100 1wj100

保证 1 ≤ n ≤ 2000 1 \leq n \leq 2000 1n2000 0 ≤ m ≤ 2000 0 \leq m \leq 2000 0m2000 1 ≤ v ≤ 300 1 \leq v \leq 300 1v300 0 ≤ e ≤ 90000 0 \leq e \leq 90000 0e90000

保证通过学校里的道路,从任何一间教室出发,都能到达其他所有的教室。

保证输入的实数最多包含 3 3 3 位小数。

输出格式

输出一行,包含一个实数,四舍五入精确到小数点后恰好 2 2 2位,表示答案。你的输出必须和标准输出完全一样才算正确。

测试数据保证四舍五入后的答案和准确答案的差的绝对值不大于 4 × 1 0 − 3 4 \times 10^{-3} 4×103。 (如果你不知道什么是浮点误差,这段话可以理解为:对于大多数的算法,你可以正常地使用浮点数类型而不用对它进行特殊的处理)

样例 #1

样例输入 #1

3 2 3 3
2 1 2
1 2 1
0.8 0.2 0.5 
1 2 5
1 3 3
2 3 1

样例输出 #1

2.80

提示

【样例1说明】

所有可行的申请方案和期望收益如下表:

【提示】

  1. 道路中可能会有多条双向道路连接相同的两间教室。 也有可能有道路两端连接的是同一间教室。
  2. 请注意区分n,m,v,e的意义, n不是教室的数量, m不是道路的数量。

特殊性质1:图上任意两点 a i a_i ai, b i b_i bi, a i a_i ai b i b_i bi间,存在一条耗费体力最少的路径只包含一条道路。

特殊性质2:对于所有的 1 ≤ i ≤ n 1≤ i≤ n 1in k i = 1 k_i= 1 ki=1

题解

一个状态机模型的dp问题。 d p [ i ] [ j ] [ k ] dp[i][j][k] dp[i][j][k]表示走完前 i i i个教室, 换了 j j j次的最优方案,以及上个方案有没有换教室。很容易得到状态转移方程。这里借用洛谷ViXbob的图和代码。

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 2e3 + 5;
const double inf = 1e17 + 5;
int n, m, v, e, c[MAXN][2], mp[305][305];
double k[MAXN], dp[MAXN][MAXN][2], ans;
inline int read() {
    char ch = getchar(); int u = 0, f = 1;
    while (!isdigit(ch)) {if (ch == '-')f = -1; ch = getchar();}
    while (isdigit(ch)) {u = u * 10 + ch - 48; ch = getchar();}return u * f;
}
int main(){
    memset(mp, 63, sizeof(mp));
    n = read(); m = read(); v = read(); e = read();
    for (register int i = 1; i <= n; i++)c[i][0] = read();
    for (register int i = 1; i <= n; i++)c[i][1] = read();
    for (register int i = 1; i <= n; i++)scanf("%lf", &k[i]);
    for (register int i = 1; i <= e; i++){
        int x = read(), y = read(), w = read();
        mp[x][y] = mp[y][x] = min(mp[x][y], w);
    }
    for (register int k = 1; k <= v; k++)
        for (register int i = 1; i <= v; i++)
            for (register int j = 1; j <= v; j++)
                mp[i][j] = min(mp[i][j], mp[i][k] + mp[k][j]);
    for (register int i = 1; i <= v; i++)mp[i][i] = mp[i][0] = mp[0][i] = 0;
    for (register int i = 0; i <= n; i++)
        for (register int j = 0; j <= m; j++)dp[i][j][0] = dp[i][j][1] = inf;
    dp[1][0][0] = dp[1][1][1] = 0;
    for (register int i = 2; i <= n; i++){
        dp[i][0][0] = dp[i - 1][0][0] + mp[c[i - 1][0]][c[i][0]];
        for (register int j = 1; j <= min(i, m); j++){
            int C1 = c[i - 1][0], C2 = c[i - 1][1], C3 = c[i][0], C4 = c[i][1];
            dp[i][j][0] = min(dp[i][j][0], min(dp[i - 1][j][0] + mp[C1][C3], dp[i - 1][j][1] + mp[C1][C3] * (1 - k[i - 1]) + mp[C2][C3] * k[i - 1]));
            dp[i][j][1] = min(dp[i][j][1], min(dp[i - 1][j - 1][0] + mp[C1][C3] * (1 - k[i]) + mp[C1][C4] * k[i], dp[i - 1][j - 1][1] + mp[C2][C4] * k[i] * k[i - 1] + mp[C2][C3] * k[i - 1] * (1 - k[i]) + mp[C1][C4] * (1 - k[i - 1]) * k[i] + mp[C1][C3] * (1 - k[i - 1]) * (1 - k[i])));
        }
    }
    ans = inf;
    for (register int i = 0; i <= m; i++)ans = min(ans, min(dp[n][i][0], dp[n][i][1]));
    printf("%.2lf", ans);
    return 0;
}

[yLOI2020] 凉凉

题目背景

凉凉三生三世恍然如梦,须臾的年风干泪痕。
若是回忆不能再相认,就让情分落九尘。
凉凉十里何时还会春盛,又见树下一盏风存。
落花有意流水无情,别让恩怨爱恨凉透那花的纯,吾生愿牵尘。

——张碧晨&杨宗纬《凉凉》

题目描述

这是 yLOI 系列竞赛中第一道以歌曲命名但歌手不是银临的题目。这道题目的歌曲和问题没什么关系,只是我们的主人公叫凉凉,于是扶苏为他选择了这首歌。

凉凉在和「七瑾在成都喝着凉茶看 jk 边咕咕边嘎嘎边哔哔边在瓦片上吭吭哧哧切企鹅」群的部分群友在青岛面基结束后,和扶苏一起乘坐地铁被七瑾送到了青岛北站。在乘坐地铁的途中,他们经过了「做物理站(错埠岭站)」,做完了高考物理的凉凉给一点都不想做物理的扶苏提了一个物理问题,扶苏不会做,所以凉凉决定考你一道经济学问题。

青岛共有 n n n 条地铁线路和 m m m 个地铁站点。每条线路的地铁都在地下以某一固定的深度运行,而如果某深度为 i i i 的地铁经过了地铁站 j j j,那么地铁站 j j j 就要在深度为 i i i 的地方挖一个站台作为上下客口,开挖该上下客口的花费为 a i , j a_{i,j} ai,j。我们忽略建设上下客口通向地面的通道的费用,而只考虑在该深度建上下客口的花费。显而易见,对于线路 u u u 和线路 v v v,如果他们都经过了同一个地铁站,那么他们线路不能处在同一深度,否则两线地铁将会相撞。而如果 u u u v v v 不存在任何一个相同的经过站点,那么这两条线既可以处在同一高度,也可以不处在同一高度。

在这个问题中,你可以认为任何两个地铁不会在除了站点以外的行驶途中相遇,也即你无需考虑两个地铁因为行驶线路交叉而在两站点之间相遇的情况。

将站点从 1 1 1 m m m 编号,线路从 1 1 1 n n n 编号,现在给定你 n n n 条线路的经过站点列表和在每个站点的每个深度的建站花费,请你求出让所有的地铁正常运行的最小建站花费总和。

输入格式

第一行有两个整数,分别表示地铁线路的数量 n n n 和站点个数 m m m
2 2 2 到第 ( n + 1 ) (n+1) (n+1) 行,每行 m m m 个整数,第 ( i + 1 ) (i+1) (i+1) 行的第 j j j 个整数表示 a i , j a_{i,j} ai,j
( n + 2 ) (n+2) (n+2) 行到第 ( 2 n + 1 ) (2n+1) (2n+1) 行,每行若干个整数,第 ( n + i + 1 ) (n+i+1) (n+i+1) 行表示地铁 i i i 号线的运行线路信息:

每行首先有一个整数 c c c,表示该线经过的站点个数,接下来该行有 c c c 个互不相同的整数 u u u,依次表示该地铁经过的站点编号。

输出格式

输出一行一个整数表示答案。

样例 #1

样例输入 #1

2 3
4 1 1
4 1 5
2 1 2
2 1 3

样例输出 #1

10

提示

样例 1 解释

1 1 1 号线和 2 2 2 号线都经过了站点 1 1 1,因此他们不能处于同一深度。
1 1 1 号线在深度 2 2 2 运行, 2 2 2 号线在深度 1 1 1 运行,则需要修建站点 1 1 1 的深度 1 1 1 2 2 2 的上下客口(花费为 4 + 4 = 8 4+4=8 4+4=8),站点 2 2 2 的深度为 2 2 2 的上下客口(花费为 1 1 1),站点 3 3 3 的深度为 1 1 1 的上下客口(花费为 1 1 1),总花费为 10 10 10。可以证明,这是最优的方案。

数据规模与约定

本题共 20 20 20 个测试点,每个测试点 5 5 5 分。

  • 对于 5 % 5\% 5% 的数据,保证 n = 1 n=1 n=1
  • 对于 35 % 35\% 35% 的数据,保证 n , m ≤ 6 n,m \le 6 n,m6
  • 对于 70 % 70\% 70% 的数据,保证 n ≤ 10 n \le 10 n10
  • 对于 100 % 100\% 100% 的数据,保证 1 ≤ n ≤ 14 1 \le n \le 14 1n14 1 ≤ m ≤ 1 0 5 1 \le m \le 10^5 1m105 1 ≤ a i , j ≤ 1 0 9 1 \le a_{i,j} \le 10^9 1ai,j109 1 ≤ c , u ≤ m 1 \le c,u \le m 1c,um

提示

本题共有两个附加样例文件,见附加文件中的 cold.zip

(本来有个更大的样例,但是因为附件不让传这么大的,就被删掉了)

题解

由于数据为十位数,所以我们想到两种方式。一种是时间上为 n ! n! n!的dfs,一种是空间上的状态压缩。dfs只能过前几个数据点,所以我们考虑用状压
这里转载一篇好题解,作者是一扶苏一
在这里插入图片描述

#include <cctype>
#include <cstdio>
#include <algorithm>

typedef long long int ll;

const int maxn = 15;
const int maxm = 100005;
const int maxt = 1 << maxn;
const ll INF = 1000000000000000000ll;

template <typename T>
inline void qr(T &x) {
  char ch = getchar();
  while (!isdigit(ch)) ch = getchar();
  while (isdigit(ch)) x = x * 10 + ch - 48, ch = getchar();
}

int n, m, upc;
int a[maxn][maxm], b[maxn][maxm], tmp[maxn];
ll fee[maxn][maxn], g[maxn][maxt], f[maxn][maxt];
bool ac[maxn][maxn];

int main() {
  qr(n); qr(m);
  upc = 1 << n;
  for (int i = 0; i < n; ++i) {
    for (int j = 1; j <= m; ++j) {
      qr(a[i][j]);
    }
  }
  for (int i = 0; i < n; ++i) {
    qr(b[i][0]);
    for (int j = 1; j <= b[i][0]; ++j) {
      qr(b[i][j]);
    }
    std::sort(b[i] + 1, b[i] + 1 + b[i][0]);
    for (int j = 0; j < n; ++j) {
      for (int k = 1; k <= b[i][0]; ++k) {
        fee[i][j] += a[j][b[i][k]];
   //     printf("Aa%d %d %d %d %d %d\n", i, j, k, b[i][k], a[j][b[i][k]], fee[i][j]);
      }
  //    printf("ovO%d %d %d\n", i, j, fee[i][j]);
    }
  }
  
  for (int i = 0; i < n; ++i) {
    for (int j = i + 1; j < n; ++j) {
      ac[i][j] = ac[j][i] = true;
      int u = 1, v = 1;
      while ((u <= b[i][0]) && (v <= b[j][0])) {
        if (b[i][u] == b[j][v]) {
          ac[j][i] = ac[i][j] = false; break;
        }
        if (b[i][u] < b[j][v]) ++u;
        else ++v;
      }
    }
  }
  for (int s = 1; s < upc; ++s) {
    int cnt = 0;
    for (int i = 0; i < n; ++i) if (s & (1 << i)) {
      tmp[++cnt] = i;
      for (int j = 1; j < cnt; ++j) if (ac[tmp[j]][i] == false) {
        for (int k = 0; k < n; ++k) {
          g[k][s] = INF;
        }
        break;
      }
      if (g[0][s] == INF) break;
      for (int k = 0; k < n; ++k) {
        g[k][s] += fee[i][k];
      }
    }
  }
  for (int s = 1; s < upc; ++s) {
    f[0][s] = g[0][s];
  }
  for (int i = 1; i < n; ++i) {
    for (int s = 0; s < upc; ++s) {
      f[i][s] = f[i - 1][s];
      for (int t = s; t; t = (t - 1) & s) {
        f[i][s] = std::min(f[i][s], f[i - 1][s ^ t] + g[i][t]);
      }
     // printf("%d %d %lld\n", i, s, f[i][s]);
    }
  }
  printf("%lld\n", f[n - 1][upc - 1]);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值