【暑期集训第一场】欧拉回路 | 思维 | 数论构造 | 容斥原理 | 线段树 | 归并排序


集训1(HDU2018 Multi-University Training Contest 2)


ID A B C D E F G H I J
AC O O
补题 O O ? O

代码 & 简易题解


[A]:期望?

神仙题,留坑..


[B]:??

\(\text{A}\)


[C]:求欧拉通路条数,以及每条的路径

小学数竞里有讲过,无向图一笔画的充要条件是有零个或两个“奇点”(偶点个数不限),“奇点”在这里就是指度为奇数的点...

其实上面两种情况就分别对应着欧拉回路和欧拉通路,并且如果把两个奇点连起来的话欧拉通路就变成欧拉回路了。

于是我们考虑本题应该也可利用这种思路。对于至少有 \(2\) 个点的无向连通块,如果它有 \(2*k\) 个奇点(注意奇点必有偶数个,因为每条边可看做贡献了两个度,所以总度是偶数,而偶点不管有多少个他们的总度都是偶数,所以奇点的总度也应该也是偶的,所以奇点就必须有偶数个),那么我们把这 \(2*k\) 个奇点任意两两连接就可以使这个连通块不含任何奇点,也就成为一个欧拉回路了。找到欧拉回路后,再断掉这 \(k\) 条新增的边,就得到 \(k\) 段欧拉通路,也就得解了。

当然,如果这个连通块只有一个点,那就特判一下,此时不存在欧拉通路。


\(\text{AC}\) 代码:

#include <cstdio>
#include <cstring>
#include <vector>

const int MV = 100007, ME = MV;


struct Ed
{
	int v, ne, id;
	bool used;
} ed[3 * ME];	// 注意这里不是平常无向图的两倍,因为还会加边!如果所有的点都是奇点就会加MV/2条双向边也就是MV条边,所以要开3倍
int head[MV], tot;
inline void edd(int u, int v, int id)
{
	ed[++tot] = {v, head[u], id, false};
	head[u] = tot;
}


int deg[MV];
bool vis[MV];
std::vector<int> path[MV];
int pnt;
void dfs(int u)
{
	vis[u] = true;
	for (int i=head[u]; i; i=ed[i].ne)
	{
		if (!ed[i].used)
		{
			ed[i].used = ed[i^1].used = true;
			dfs(ed[i].v);
			
			if (ed[i].id == 0)	// 遇到了补的边
				++pnt;
			else
				path[pnt].push_back(-ed[i].id);
		}
	}
}


int main()
{
	int V, E;
	while (~scanf("%d %d", &V, &E))
	{
		tot = 1;
		pnt = 0;
		memset(deg, 0, sizeof(*deg) * (V+1));
		memset(vis, 0, sizeof(*vis) * (V+1));
		memset(head, 0, sizeof(*head) * (V+1));
		
		for (int i=1; i<=E; ++i)
		{
			int u, v;
			scanf("%d %d", &u, &v);
			++deg[u], ++deg[v];
			edd(u, v, i), edd(v, u, -i);
		}
		
		static int odd_v[MV];
		int odd_t = 0;
		for (int v=1; v<=V; ++v)
			if (deg[v] & 1)
				odd_v[odd_t++] = v;
		for (int i=0; i<odd_t; i+=2)
			edd(odd_v[i], odd_v[i+1], 0), edd(odd_v[i+1], odd_v[i], 0);
		
		// 此时每个连通块都是一个单点or一个欧拉回路
		// 先遍历那些有奇点的(曾经补过边的)连通块
		for (int v=1; v<=V; ++v)
			if (!vis[v] && (deg[v]&1))
				++pnt, dfs(v), --pnt;

		// 再遍历剩下的的连通块(注意跳过单点连通块,否则会出现长度为0的路径,这是本题不需要输出的)
		for (int v=1; v<=V; ++v)
			if (!vis[v] && deg[v])
				++pnt, dfs(v);

		printf("%d\n", pnt);
		for (int i=1; i<=pnt; ++i)
		{
			printf("%d ", path[i].size());
			for (auto p : path[i])
				printf("%d ", p);
			puts("");
			path[i].clear();
		}
	}

	return 0;
}


[D]:思维

两个人玩游戏,轮流每次从 \(\{1, 2, 3, ..., n\}\) 的集合中选一个数,然后把集合中存在的这个数的所有因子剔除(包括自身),如果某个人无法再操作(面对∅)则输掉游戏,问是否先手必胜?

答案是肯定的。若 \(n==1\),那显然先手必胜。若 \(n>1\),先考虑 \(\{2, 3, ..., n\}\),如果面对这一批后手有必胜策略,则加上 \(1\) 之后先手者第一步拿 \(1\),那么就先手必胜了;而如果先手者对 \(\{2, 3, ..., n\}\) 已经有必胜策略,那么算上 \(1\) 后还是按原来必胜策略来即可必胜,因为先手者不管拿什么非 \(1\) 都会拿走 \(1\)

\(\text{AC}\) 代码(C):

main(){while(~scanf("%*d"))puts("Yes");}

[E]:数论构造


\(\text{AC}\) 代码:



[F]:容斥


\(\text{AC}\) 代码:



[G]:线段树

有两个长度均为 \(n\) 的数组 \(a, b\)\(a\) 初始时全为 \(0\)\(b\) 为给定的一个 \(1\sim n\) 的排列
\(q\) 次操作,有两种,一是把 \(a\) 区间 \([l, r]\)\(1\),二是求 \(\sum\limits_{i=l}^{r}\lfloor \frac{a_i}{b_i} \rfloor\)

区间加和区间求和,能想办法用线段树维护吗?这里需要转一下脑筋...要充分利用取整操作答案更新的周期性和离散性

我们维护一个 \(c\) 数组和 \(sum\) 数组,初始时 \(c=b,sum=\{0\}\),每次进行第一种操作时就对 \(c\) 的对应区间减 \(1\)。减完之后,看看此区间内 \(c\) 的最小值是否为 \(0\),若为 \(0\) 则查到是哪个(些)点,查到这些减到 \(0\) 的位置(记组成集合 \(S\)),此时这些地方的 \(c[i](i\in S)\) 已经累计减少了 \(b[i]\) 次了(等价于 \(a[i]\) 累计增加了 \(b[i]\) 次),于是就把 \(sum[i]\) 加一,然后再把 \(c[i]\) 重新赋上 \(b[i]\) 的值。这样,每次查询求和式的时候,只需查询 \(sum[i]\) 的区间和即可。

所以这就可以用线段树维护了。

再看看复杂度有没有保证:维护 \(sum\) 肯定是保证 \(O(q\log n)\) 的,不过区间减一的时候可能还涉及叶节点查询,这样复杂度可能会爆炸。但考虑到查询叶节点最多不超过 \(\sum\limits_{i=1}^{n}\frac{q}{i}\) 次,这货又是调和级数是和 \(\log(q)\) 同阶的,所以区间更新的复杂度是保证 \(O(q\log^2 n)\) 的。整体复杂度 \(O(q\log^2 n)\),可以接受。


\(\text{AC}\) 代码:

#include <cstdio>

#define MIN(a, b) ((a)<(b)?(a):(b))

const int MN = 100007;

typedef int vint, xint, sint;
const xint ROOT = 1;


class STree
{
private:
	struct Node
	{
		xint l, r;
		sint min, s, lza;
	} t[MN << 2];
	vint *arr;
	
	xint ll, rr;

#define li i<<1
#define ri i<<1|1
#define t_mid ((t[i].l+t[i].r) >> 1)

#define add_v(i, v) \
	({ \
		t[i].min += v, \
		t[i].lza += v; \
	})

#define pd(i) \
	({ \
		if (t[i].lza) \
		{ \
			add_v(li, t[i].lza); \
			add_v(ri, t[i].lza); \
			t[i].lza = 0; \
		} \
	})

#define pu(i) \
	({ \
		t[i].min = MIN(t[li].min, t[ri].min), \
		t[i].s = t[li].s + t[ri].s; \
	})

	void build(const xint i, const xint l, const xint r)
	{
		t[i].l = l, t[i].r = r, t[i].lza = 0;
		if (l == r)
		{
			t[i].min = arr[r];
			t[i].s = 0;
		}
		else
		{
			build(li, l, t_mid);
			build(ri, t_mid+1, r);
			pu(i);
		}
	}

	void dec(const xint i)
	{
		if (ll <= t[i].l && t[i].r <= rr && t[i].min > 1)
			add_v(i, -1);
		else
		{
			if (t[i].l == t[i].r && t[i].min == 1)
			{
				t[i].min = arr[t[i].r];
				t[i].s += 1;
			}
			else
			{
				pd(i);
				if (ll <= t_mid)
					dec(li);
				if (rr > t_mid)
					dec(ri);
				pu(i);
			}
		}
	}
	
	sint sum(const xint i)
	{
		if (ll <= t[i].l && t[i].r <= rr)
			return t[i].s;
		else
		{
			pd(i);
			sint s = 0;
			if (ll <= t_mid)
				s += sum(li);
			if (rr > t_mid)
				s += sum(ri);
			return s;
		}
	}
	
public:
	void build(vint *a, const xint l, const xint r)
	{
		arr = a;
		build(ROOT, l, r);
	}

	void dec(const xint l, const xint r)
	{
		ll = l, rr = r;
		dec(ROOT);
	}

	sint sum(const xint l, const xint r)
	{
		ll = l, rr = r;
		return sum(ROOT);
	}
};

STree st;
int b[MN];

int main()
{
	char op[9];
	int n, q, l, r;
	while (~scanf("%d %d", &n, &q))
	{
		for (int i=1; i<=n; ++i)
			scanf("%d", b+i);
		st.build(b, 1, n);
		while (q--)
		{
			scanf("%s %d %d", op, &l, &r);
			if (*op == 'a')
				st.dec(l, r);
			else
				printf("%d\n", st.sum(l, r));
		}
	}
	
	return 0;
}


[J]:思维+归并排序

给定一个长度 \(n\) 的数组 \(a\),数组中每存在一个逆序对(\(i<j,\ a[i]>a[j]\))则需付出 \(x\) 元的代价。
你可以进行任意多次的相邻交换操作,每次操作代价是 \(y\) 元。
求进行任意次操作后能达到的最小代价。

考虑相邻交换操作,每次操作最多消除一个逆序对,因此如果 \(y>x\),做操作是肯定会亏的;而如果 \(y==x\),做操作最多也就保证不会付出更多代价,不可能减少代价。

因此只有当 \(y<x\) 时我们才有可能通过相邻交换操作减少代价。我们能不能做到每次操作必消除一个逆序对呢?其实是可以的。我们每次从后往前找第一个相邻的逆序对(和prev_permutation的第一步是一样的),如果找不到则说明序列已经有序;如果找到,则把这个逆序对靠前的那个数一直往后挪,每挪一次(进行一次相邻交换操作)就消除了一个逆序对,一直挪到不能消除逆序对为止。显然完成这么一轮后,后面这一段仍然保持着单调不减性。于是一直这样操作下去整个序列也就单调不减了,也就排完序了,并且整个过程中每次相邻交换都消除了一个逆序对。

因此我们是可以做到每次操作都消除一个逆序对的。因此 \(y<x\) 时最少付出的代价就是 \(y*\)逆序对个数。

综上可以发现,题目的答案正是 \(\min(x, y)*\)逆序对个数。可以用归并排序 / 权值树状数组 / 平衡树来做。


\(\text{AC}\) 代码:

#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
#include<queue>
#include<map>
#define N 200010
#define LL long long
#define INF 0x3f3f3f3f
using namespace std;

int n,i,a,b,be;
int c[N],d[N];
LL Ans,cnt;

inline int Abs(int x){return (x<0)?-x:x;}
inline int Min(int a,int b){return (a<b)?a:b;}
inline int Max(int a,int b){return (a>b)?a:b;}

inline int read(){
	int p=0;	char	c=getchar();
	while (c<48||c>57)	c=getchar();
	while (c>=48&&c<=57)	p=(p<<1)+(p<<3)+c-48,c=getchar();
	return p;
}

inline void Merge(int low,int high){
	int mid=(low+high)>>1,l=0,r=0,e=0;
	if (low==high)	return;
	Merge(low,mid);	Merge(mid+1,high);
	l=low;	r=mid+1;	e=low;
	while (l<=mid&&r<=high){
		if (c[l]<=c[r])	d[e++]=c[l++];
		else {
			Ans+=(mid-l+1);	d[e++]=c[r++];
		}
	}
	while (l<=mid)	d[e++]=c[l++];
	while (r<=high)	d[e++]=c[r++];
	for (i=low;i<=high;i++)	c[i]=d[i];
}

int main(){
//	freopen("zht.in","r",stdin);
//	freopen("zht.out","w",stdout);
	while (~scanf("%d%d%d",&n,&a,&b)){
		if (n==1){
			cout<<"0"<<endl;
			continue;
		}
		for (i=1;i<=n;i++)	scanf("%d",&c[i]);
		Ans=0;	Merge(1,n);	cnt=Min(a,b);
		cout<<(LL)Ans*cnt<<endl;
	}
	return 0;
}

补题方案

  • E、G 较简单,赛后应马上补上。

  • C、F 属于常见知识点,也应尽快补上。

  • 其余神仙题...待进一步提升实力后再回来填坑吧QAq


总结

  • 死磕没有结果时,应该去跟榜...

posted @ 2019-08-06 23:45  _403Forbidden  阅读(139)  评论(0编辑  收藏  举报