『模拟赛题解』10.3 NOIP 模拟赛

10.3 NOIP 模拟赛

T1. 不稳定的道路

Description

\(n\) 个城市和 \(m\) 条道路。城市编号 \(1\)\(n\) ,道路编号 \(1\)\(m\) 。道路 \(i\) 双向连接城市 \(a_i\) 和城市 \(b_i\)

但是通过每一条道路,所需的时间却是不稳定的,跟出发的时刻有关,如果在时刻 \(t\) 通过道路 \(i\) ,那么需要的时间为:\(c_i + \lfloor \! \frac{d_i}{t + 1} \! \rfloor\) 。其中 \(c_i\)\(d_i\) 是给出的整数,并且上面这个式子的计算需要向下取整。

你计划从城市 \(1\) 去往城市 \(n\) ,这个过程中你可以在任何城市进行停留(不必立即出发)。问最早到达城市 \(n\) 的时间。如果无法到达城市 \(n\),请输出 \(-1\)

Solution

题目中边权的计算公式是 \(c_i + \lfloor \! \frac{d_i}{t + 1} \! \rfloor\),其中 \(c_i\) 不会改变。对于后面的部分,不难发现当 \(t > \sqrt{d_i} + 1\) 时,直接走显然最优,否则就等到 \(\sqrt{d_i} + 1\)。所以直接对于每种情况的 \(t\) 建边,跑最短路就行。

Code

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

#define int long long

const int maxn = 1e5 + 10;

int n, m, cnt;
int head[maxn];

struct Edge
{
    int to, nxt, c, d, g;
} edge[maxn * 2];

priority_queue < pair <int, int> > q;
int dis[maxn];
bool vis[maxn];

void addedge(int x, int y, int c, int d, int g)
{
    edge[++ cnt] = (Edge){y, head[x], c, d, g};
    head[x] = cnt;
}

signed main()
{
    freopen("road.in", "r", stdin);
    freopen("road.out", "w", stdout);
    cin >> n >> m;
    for (int i = 1; i <= m; i ++)
    {
        int x, y, c, d;
        cin >> x >> y >> c >> d;
        int g = (int)sqrt(d);
        if (d - g * g  > (g + 1) * (g + 1) - d)
            g ++;
        g --;
        addedge(x, y, c, d, g);
        addedge(y, x, c, d, g);
    }

    for (int i = 1; i <= n; i ++)
        dis[i] = 1e18;
    dis[1] = 0;
    q.push(make_pair(0, 1));
    while (!q.empty())
    {
        int t = -q.top().first;
        int x = q.top().second;
        q.pop();
        if (vis[x])
            continue;
        vis[x] = 1;
        for (int i = head[x]; i; i = edge[i].nxt)
        {
            int y = edge[i].to, g = edge[i].g;
            if (vis[y])
                continue;
            if (t <= g) 
            {
                if (dis[y] > g + edge[i].c + edge[i].d / (g + 1)) 
                {
                    dis[y] = g + edge[i].c + edge[i].d / (g + 1);
                    q.push(make_pair(-dis[y], y));
                }
            }
            else 
            {
                if (dis[y] > t + edge[i].c + edge[i].d / (t + 1))
                {
                    dis[y] = t + edge[i].c + edge[i].d / (t + 1);
                    q.push(make_pair(-dis[y], y));
                }
            }
        }
    }
    if (dis[n] == 1e18)
        cout << -1;
    else 
        cout << dis[n];
    return 0;
}

T2. 小 A 的数

Description

给出一棵 \(n\) 个点的树,每个点有黑白两种颜色。

\(q\) 次询问,每次询问给出 \(x\)\(y\) ,问能否选出一个 \(x\) 个点的联通子图,使得其中黑点数目为 \(y\)

Solution

发现 \(n \le 5000\),所以直接预处理显然不行。

所以考虑树形 dp,显然对于某一大小的连通子图,其包含黑点数的最小值与最大值之间的所有点数目都能够取得到(一个连通子图删除一个点再加入一个点后,黑点的数目变化最多只为 \(1\) 。因此可以变化到\([\min, \,\max]\) 之间所有的数目)。

考虑树形背包,设 \(f_{i, j}\) 表示从 \(i\) 的子树中选出大小为 \(j\) 的连通子图黑点的最小值;\(g_{i, j}\) 表示最大值。用树形背包转移。

注意只能使用已经遍历过的点数目和当前子树中的点数目转移,否则在遇到链时会退化成 \(O(n^3)\)

Code

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

const int maxn = 5e3 + 5;

int n, q, cnt;
int head[maxn], col[maxn], sz[maxn], f[maxn][maxn], g[maxn][maxn];
struct Edge
{
	int nxt, to;
} edge[maxn << 1];

void addedge(int x, int y)
{
	edge[++ cnt].to = y;
	edge[cnt].nxt = head[x];
	head[x] = cnt;
}

void dfs(int x, int fa)
{
	sz[x] = 1;
	f[x][1] = g[x][1] = col[x];
	for (int i = head[x]; i; i = edge[i].nxt)
	{
		int y = edge[i].to;
		if (y == fa)
			continue;
		dfs(y, x);
		for (int j = sz[x]; j; j --)
			for (int k = sz[y]; k; k --)
			{
				f[x][j + k] = min(f[x][j + k], f[x][j] + f[y][k]);
				g[x][j + k] = max(g[x][j + k], g[x][j] + g[y][k]);
			}
		sz[x] += sz[y];
	}
	for (int i = 1; i <= n; i ++)
	{
		f[0][i] = min(f[0][i], f[x][i]);
		g[0][i] = max(g[0][i], g[x][i]);
	}
}

int main()
{
	freopen("tree.in", "r", stdin);
	freopen("tree.out", "w", stdout);
	int t;
	cin >> t;
	cin >> n >> q;
	for (int i = 1; i < n; i ++)
	{
		int x, y;
		cin >> x >> y;
		addedge(x, y);
		addedge(y, x);
	}
	for (int i = 1; i <= n; i ++)
		cin >> col[i];
	memset(f, 0x3f, sizeof f);
	dfs(1, 0);
	while (q --)
	{
		int x, y;
		cin >> x >> y;
		if (y >= f[0][x] && y <= g[0][x])
			cout << "YES\n";
		else
			cout << "NO\n";
	}
	return 0;
}

T3. 吵架

Description

老虎和蒜头是好朋友,但他们经常吵架。吵完架之后,两人会各自去到一个僻静的角落,使得它们的距离最远。

他们所在的小镇有 \(n\) 个角落,有 \(n - 1\) 条长度为 \(1\) 的道路每条连接两个角落,角落互相可达;两个角落的距离等于它们之间简单路径的长度;起初所有的角落都是僻静的。

\(1\)\(q\)\(q\) 天,每天会发生一个事件,用以下两者之一描述:

  1. \(C \; x\) :角落 \(x\) 的僻静性发生反转(原来僻静则现在吵闹,原来吵闹则现在僻静)
  2. \(G\) :老虎和蒜头吵了一次架,你需要输出当它们各自去到想去的僻静角落之后它们之间的距离。这对角 落应是所有僻静角落对中相距最远的一对。特别地,如果只有一个僻静的角落,输出 \(0\) ;如果所有角落都吵闹,输出 \(-1\)

Solution

\(n\) 个点,\(n - 1\) 条边的连通图,这显然是一棵树。

则操作 2 就变成了求树的直径(两个端点都是僻静的角落)。但是看到数据范围,只能用 \(O(n \log n)\) 的东西去维护直径了,恰好昨天又练了一下午加一晚上的线段树,很容易想到了用线段树来维护直径。

怎么维护呢?线段树的每个区间 \([l, \,r]\) 表示点集 \([l, \,r]\) 的直径。吵闹的点设成 \([-1, \,-1]\),僻静的点设成 \([x, \,x]\)

Code

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

const int maxn = 1e6 + 5;

int n, q, cnt;
int dep[maxn], head[maxn];
int f[maxn][20];
int flag[maxn];
int lg2[maxn];
bool vis[maxn];
struct Edge
{
	int to, nxt;
} edge[maxn];
struct node
{
	int l, r, a = -1, b = -1;
} tree[maxn * 4];

void addedge(int x, int y)
{
	edge[++ cnt].to = y;
	edge[cnt].nxt = head[x];
	head[x] = cnt;
}

void dfs(int x, int fa)
{
	if (vis[x])
		return ;
	vis[x] = true;
	dep[x] = dep[fa] + 1;
	f[x][0] = fa;
	for (int i = 1; i <= lg2[dep[x]]; i ++)
		f[x][i] = f[f[x][i - 1]][i - 1];
	for (int i = head[x]; i; i = edge[i].nxt)
		dfs(edge[i].to, x);
}

int lca(int x, int y)
{
	if (dep[x] < dep[y])
		swap(x, y);
	while (dep[x] != dep[y])
		x = f[x][lg2[dep[x] - dep[y]]];
	if (x == y)
		return x;
	for (int k = lg2[dep[x]]; k >= 0; k --)
		if (f[x][k] != f[y][k])
		{
			x = f[x][k];
			y = f[y][k];
		}
	return f[x][0];
}

int dis(int x, int y)
{
	if (x == -1 || y == -1)
		return -0x3f3f3f3f + x + y;
	return dep[x] + dep[y] - 2 * dep[lca(x, y)];
}

void pushup(int q)
{
	tree[q].a = tree[q].b = -1;
	int p[4] = {tree[q << 1].a, tree[q << 1].b, tree[q << 1 | 1].a, tree[q << 1 | 1].b};
	for(int i = 0;i < 4;++ i)
		for(int j = 0;j < i;++ j){
		int x = p[i], y = p[j];
		if(dis(x, y) > dis(tree[q].a, tree[q].b))
			tree[q].a = x, tree[q].b = y;
	}	
}

void build(int q, int l, int r)
{
	tree[q].l = l;
	tree[q].r = r;
	if(l == r)
	{
		tree[q].a = tree[q].b = l;
		return;
	}
	int mid = l + r >> 1;
	build(q << 1, l, mid);
	build(q << 1 | 1, mid + 1, r);
	pushup(q);
}

void modify(int q, int l, int r, int k)
{
	if (tree[q].l >= l && tree[q].r <= r)
	{
		tree[q].a = tree[q].b = k;
		return ;
	}
	int mid = tree[q].l + tree[q].r >> 1;
	if (mid >= l)
		modify(q << 1, l, r, k);
	if (mid < r)
		modify(q << 1 | 1, l, r, k);
	pushup(q);
}

int main()
{
	freopen("quarrel.in", "r", stdin);
	freopen("quarrel.out", "w", stdout);
	cin >> n >> q;
	for(int i = 1;i < n; i ++)
	{
		int a, b;
		cin >> a >> b;
		addedge(a, b);
		addedge(b, a);
	}
	for (int i = 2; i <= n; i ++)
		lg2[i] = lg2[i / 2] + 1;
	dfs(1, 0);
	build(1, 1, n);
	
	int num = n, x;
	for (int i = 1;i <= q; i ++)
	{
		char op;
		cin >> op;
		if(op == 'G')
		{
			if (num == 1)
				cout << "0\n";
			else if (num == 0)
				cout << "-1\n";
			else
				cout << dis(tree[1].a, tree[1].b) << endl;
		}
		else
		{
			cin >> x;
			flag[x] ^= 1;
			if (flag[x] == 1)
				num --, modify(1, x, x, -1);
			else
				num ++, modify(1, x, x, x);
		}
	}
	return 0;
}

T4. 选数问题 V2

Description

给一个长度为 \(n\) 的数组 \(a\) ,数据保证每个 \(a_i\) 的约数数量不超过 \(7\) ,求最少选出多少个数(同一个数不能选 \(2\) 次),使得选出的数乘积为完全平方数。无解输出 \(-1\)

Solution

由于每个数的约数数量小于等于 \(7\) ,因此可知每个数的不同质因子数量小于 \(3\) ,否则约数数量至少是 \(8\) 。也就是说这些数的质因子数量最多只有 \(2\) 个。

同时对于每个数,去掉其中的平方因子,例如:\(8\) 变为 \(2\) ,不会影响最终的结果。

对于去除平方因子后的数,我们分类讨论:

  1. 存在数字 \(1\) ,则直接返回 \(1\)
  2. 对于质数(只有 \(1\) 个质因子的数),如果成对出现,则返回 \(2\)
  3. 对于有 \(2\) 个质因子的数,我们以质因子作为点(包括 \(1\) ),数字作为边建图(质数则是 \(1\)\(p\) 的边),然后在图中找出一个最小的环就是答案。

由于小于 \(10^6\) 的质数数量,接近 \(8 \times 10^4\) 个,因此如果用 floyd 求解是 \(O(n^3)\) 的,考虑优化。

由于 \(a_i \le 10^6\) ,因此 \(a_i\) 不存在两个都大于 \(10^3\) 的质因子,因此如果存在环,那么环一定经过小于 \(10^3\) 的点。因此我们将找全局最小环的问题转为找以小于 \(10^3\) 的质因子为起点的最小环。

在固定起点的情况下,我们可以直接用 BFS 来处理(因为权值为 \(1\) )。BFS 的过程中记录节点的深度 \(d_i\) ,如果发现某个节点已经被访问过,则一定存在环,环的长度可以简单记为 \(d_i + d_j + 1\) ,其中 \(i, j\) 是当前访问的边的两个端点,这样单次处理的时间复杂度为 \(O(n)\)

我们考虑,有时直接用 \(d_i + d_j + 1\) 来计算最小环的长度,是不正确的,例如:

\(5\) 开始做 BFS,到 \(1\) 的距离是 \(3, 2\) ,按照 \(d_i + d_j + 1\) 计算环长是 \(6\) ,不过我们是枚举所有点为起点进行计算的,当我们枚举点 \(7\) 时,就可以得到那个最小环的长度了。

一个小小的优化是,当我们当前的 BFS 不能得到小于当前最优的答案时就可以退出了,因为后面不会 得到更优的结果了。时间复杂度为 \(O(\dfrac{n\sqrt n}{\log n})\)

Code

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

const int maxn = 1e6 + 1;

int n, cnt, x, s, ans = 0x3f3f3f3f;
bool flag;
int a[maxn], q[maxn], d[maxn], p[maxn];
vector <int> g[maxn];

int main()
{
	freopen("choose.in", "r", stdin);
	freopen("choose.out", "w", stdout);
	cin >> n;
	for (int i = 1; i <= n; i ++)
		cin >> a[i];	
	for (int i = 1; i <= n; i ++)
	{
		cnt = 0;
		q[2] = 1;
		for (int j = 2; j * j <= a[i]; j ++)
		{
			x = 0;
			while (a[i] % j == 0)
			{
				a[i] /= j;
				x ++;
			}
			if (x & 1)
				q[++ cnt] = j;
		}
		if (a[i] > 1)
			q[++ cnt] = a[i];
		if (cnt == 0)
			ans = 1;
		g[q[1]].push_back(q[2]);
		g[q[2]].push_back(q[1]);
	}
	for (int i = 1; i <= 1000; i ++)
	{
		for (int j = 1; j < maxn; j ++)
			d[j] = 0x3f3f3f3f;
		d[i] = 0;
		s = cnt = 0;
		q[++ cnt] = i;
		while (s < cnt)
		{
			x = q[++ s];
			for (int j = 0; j < g[x].size(); j ++)
			{
				if (g[x][j] != p[x])
				{
					if (d[g[x][j]] == 0x3f3f3f3f)
					{
						d[g[x][j]] = d[x] + 1;
						p[g[x][j]] = x;
						q[++ cnt] = g[x][j];
					}
					else
						ans = min(ans, d[x] + d[g[x][j]] + 1);
				}
			}
		}
	}
	if (ans == 0x3f3f3f3f)
		cout << -1;
	else
		cout << ans;
	return 0;
}
posted @ 2023-10-03 19:34  Clyfort  阅读(59)  评论(0编辑  收藏  举报