NOIP2018 集训(三)

A题 Tree

问题描述

给定一颗 \(n\) 个点的树,树边带权,试求一个排列 \(P\) ,使下式的值最大

\[\sum_{i=1}^{n-1} maxflow(P_i, P_{i+1}) \]

其中 \(maxflow(s, t)\) 表示从点 \(s\) 到点 \(t\) 之间的最大流,即从 \(s\)\(t\) 的路径上最小的边权

输入格式

第一行一个整数 \(n\) ,表示点数

下接 \(n - 1\) 行,每行三个数 \(u, v,w\) 表示一条连接点 \(u\) 和点 \(v\) 权值为 \(w\) 的边

输出格式

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

数据范围

对于前 \(5\%\) 的数据满足 \(n \leq 8\)

对于前 \(40\%\) 的数据满足 \(n \leq 200\)

对于前 \(60\%\) 的数据满足 \(n \leq 2000\)

对于 \(100\%\) 的数据满足 \(n \leq 100000\)

样例

样例输入
2
1 2 2333
样例输出
2333

题解

事实上是一道十行代码就可以解决的题目
如果要让\(\sum_{i=1}^{n-1}maxflow(P_i, P_{i+1})\)的边权值最小,就要让每一条边只被经过一次,那么将所有边的权值相加就好了QAQ(网上的题解都是些什么玩意。。。)

#include<bits/stdc++.h>
using namespace std;
long long n,ans,u,v,w;
int main(){
	cin>>n;
	for(register long long i=1;i<n;i++)cin>>u>>v>>w,ans+=w;
	cout<<ans<<endl;
	return 0;
}

#### B题 Permutation #### 问题描述 给定一张 $n$ 个点 $m$ 条边的无向图,每条边连接两个顶点,保证无重边自环,不保证连通

你想在这张图上进行若干次旅游,每次旅游可以任选一个点 \(x\) 作为起点,再走到一个与 \(x\)
接有边相连的点 \(y\) ,再走到一个与 \(y\) 直接有边相连的点 \(z\) 并结束本次旅游

作为一个旅游爱好者,你不希望经过任意一条边超过一次,注意一条边不能即正向走一次又反
向走一次,注意点可以经过多次,在满足此条件下,你希望进行尽可能多次的旅游,请计算出最多
能进行的旅游次数并输出任意一种方案

输入格式

\(1\) 行两个正整数 \(n\)\(m\) ,表示全图的点数与边数
下接 \(m\) 行,每行两个数字 \(u\)\(v\) 表示一条边

输出格式

\(1\) 行一个整数 \(cnt\) 表示答案

下接 \(cnt\) 行,每行三个数字 \(x\) , \(y\)\(z\) ,表示一次旅游的路线

如有多种旅行方案,任意输出一种即可

数据范围

对于前 \(20\%\) 的数据, \(n < 10,m \leq 20\) .

对于令 \(20\%\) 的数据, \(m = n - 1\) ,并且图连通

对于令 \(10\%\) 的数据,每个点的度数不超过 \(2\)

对于 \(100\%\) 的数据, \(n \leq 100000,m \leq 200000\)

样例

样例输入
4 5
1 2
3 2
2 4
3 4
4 1
样例输出
2
4 1 2
4 3 2

题解

部分分方法很容易想到,DFS或者BFS直接遍历查找即可。

#include <bits/stdc++.h>
using namespace std;
const int maxn = 100010,maxm = 200010;
/*
struct edge{
	int v,next;
	bool used;
}E[maxm*2];
*/
struct anss{
	int a,b,c;
};
vector<int> G[maxn];
//priority_queue< pair<int,int> , vector<pair<int,int>>, less<pair<int,int>> > pq;
int p[maxn],eid;
int degree[maxn];
/*
void init(){
	memset(p,-1,sizeof p);
	eid = 0;
}
*/
inline void insert(int u,int v){
	G[u].push_back(v);
}
inline void insert2(int u,int v){
	insert(u,v);
	insert(v,u);
}
int n,m;
inline bool cmp(const int &a,const int &b){
	if(degree[a] == degree[b]){
		return a < b;
	}else{
		return degree[a] < degree[b];
	}
}
int main(){
	scanf("%d%d",&n,&m);
	memset(degree,0,sizeof degree);
	for(int i=1;i<=m;i++){
		int u,v;
		scanf("%d%d",&u,&v);
		insert2(u,v);
		degree[u]++;
		degree[v]++;
	}
	for(int i=1;i<=n;i++){
		sort(G[i].begin(),G[i].end(),cmp);
	}
	int cnt = 0;
	//int tcnt[maxn] = {0};
	vector<anss> ansss;
	for(int i=1;i<=n;i++){
		while(degree[i] >= 2){
			//cout << "i:" << i << " degree: " << degree[i] << endl;
			//ansss.push_back({G[i][tcnt[i]*2-2],i,G[i][tcnt[i]*2-1]});
			int son1 = -1,son2 = -1;
			int savep;
			int pp = 0;
			sort(G[i].begin(),G[i].end(),cmp);
			//cout << "savep:" << savep << endl;
			son1 = G[i][0],son2 = G[i][1];
			//cout << "son1:" << son1 << "son2:" << son2 << endl;
			for(int ppp=0;ppp<G[son1].size();ppp++){
				if(G[son1][ppp] == i){
					//cout << "ppp1:" << ppp << endl;
					G[son1].erase(G[son1].begin() + ppp);
					break;
				}
			}
			
			for(int ppp=0;ppp<G[son2].size();ppp++){
				if(G[son2][ppp] == i){
					//cout << "ppp1:" << ppp << endl;
					G[son2].erase(G[son2].begin()+ppp);
					break;
				}
			}
			G[i].erase(G[i].begin()+1);
			G[i].erase(G[i].begin());
			ansss.push_back({son1,i,son2});
			degree[i] -= 2;
			degree[son1]--;
			degree[son2]--;
			cnt++;
		}
	}
	printf("%d\n",cnt);
	for(int i=0;i<ansss.size();i++){
		printf("%d %d %d\n",ansss[i].a,ansss[i].b,ansss[i].c);
	}
	return 0;
}

这种方法的错误性也很容易发现。任意一个强连通图都有可能导致错误,即使是部分强联通也会出导致错误。所以我们需要找其他的思路。
首先我们可以发现,答案输出的第一行总是所有联通块中的边集\(\Sigma{m/2}\),此时我们在每个联通块中任选一点做生成树,并从根节点向下遍历,每找到两条相连边就记录一次即可。当然遍历的顺序也很讲究,要先查叶子节点的所连的边再查父亲节点的所连的边,这样才能玄学保证最优

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll maxn=4e5+10;
inline char get(){
	static char buf[30],*p1=buf,*p2=buf;
	return p1==p2 && (p2=(p1=buf)+fread(buf,1,30,stdin),p1==p2)?EOF:*p1++;
}
inline int read(){
	register char c=get();register int f=1,_=0;
	while(c>'9' || c<'0')f=(c=='-')?-1:1,c=get();
	while(c<='9' && c>='0')_=(_<<3)+(_<<1)+(c^48),c=get();
	return _*f;
}
ll n,m,num[maxn],fa[maxn];
bool vis[maxn];
struct edge{
	ll v;
	ll next;
}e[maxn];
ll p[maxn],t=0;
ll edge[maxn];
struct Path{
	ll a,b,c;
};
vector<Path> ans;
void insert(ll u,ll v){
	e[t].v=v;
	e[t].next=p[u];
	p[u]=t++;
}
void insert2(ll u,ll v){
	insert(u,v);
	insert(v,u);
}
void BFS(ll rt){
	ll x,H=0,T=1;
	num[T]=rt;
	fa[rt]=-1;
	while(H-T){
		ll u=num[++H];
		for(ll i=p[u];i!=-1;i=e[i].next){
			ll v=e[i].v;
			if(!fa[v]){
				fa[v]=u;
				num[++T]=v;
			}
		}
	}
	for(x=T;x>=1;x--){
		ll u=num[x];
		edge[0]=0;
		for(ll i=p[u];i!=-1;i=e[i].next){
			ll v=e[i].v;
			if(!vis[i] && v!=fa[u])
				edge[++edge[0]]=i;
		}
		for(ll i=p[u];i!=-1;i=e[i].next){
			ll v=e[i].v;
			if(!vis[i] && v==fa[u])
				edge[++edge[0]]=i;
		}
		for(ll i=1;i+1<=edge[0];i+=2){
			vis[edge[i]]=vis[edge[i]^1]=true;
			vis[edge[i+1]]=vis[edge[i+1]^1]=true;
			ans.push_back((Path){e[edge[i]].v,u,e[edge[i+1]].v});
		}
	}
}
int main(){
	freopen("T1.txt","r",stdin);
	memset(p,-1,sizeof(p));
	n=read(),m=read();
	for(ll i=1;i<=m;i++){
		insert2(read(),read());
	}
	for(ll i=1;i<=n;i++)
		if(!fa[i])BFS(i);
	printf("%lld\n",(ll)ans.size());
	for(ll i=0;i<ans.size();i++)printf("%lld %lld %lld\n",ans[i].a,ans[i].b,ans[i].c);
	return 0;
}

C题 Permutation

问题描述

你有一个长度为 \(n\) 的排列 \(P\) 与一个正整数 \(K\)

你可以进行如下操作若干次使得排列的字典序尽量小

对于两个满足 \(\left|i-j\right| \geq K\)\(\left|P_i-P_j\right| =1\)的下标 \(i\)\(j\) ,交换 \(P_i\)\(P_j\)

输入格式

第一行包括两个正整数 \(n\)\(K\)

第二行包括 \(n\) 个正整数,第 \(i\) 个正整数表示 \(P_i\)

输出格式

输出一个新排列表示答案

输出共 \(n\) 行,第 \(i\) 行表示 \(P_i\)

数据范围

对于前 \(20\%\) 的数据满足 \(n \leq 6\)

对于前 \(50\%\) 的数据满足 \(n \leq 2000\)

对于 \(100\%\) 的数据满足 \(n \leq 500000\)

样例

样例输入
8 3
4 5 7 8 3 1 2 6
样例输出
1
2
6
7
5
3
4
8

题解

非常好的暴力训练题。考试的时候明显是在比谁骗的分多Orz
首先是大家都懂的20分普通暴力

#include <bits/stdc++.h>
#define maxn 500010
using namespace std;
inline char get(){
	static char buf[30],*p1=buf,*p2=buf;
	return p1==p2 && (p2=(p1=buf)+fread(buf,1,30,stdin),p1==p2)?EOF:*p1++;
}
inline int read(){
	register char c=get();register int f=1,_=0;
	while(c>'9' || c<'0')f=(c=='-')?-1:1,c=get();
	while(c<='9' && c>='0')_=(_<<3)+(_<<1)+(c^48),c=get();
	return _*f;
}
struct edge{
	int u,v,w,next;
}E[maxn<<1],A[maxn<<1];
int p[maxn],eid=0;
void init(){
	for(register int i=0;i<maxn;i++)p[i]=-1;
	eid=0;
}
void insert(int u,int v,int w){
	E[eid].u=u;
	E[eid].v=v;
	E[eid].w=w;
	E[eid].next=p[u];
	p[u]=eid++;
}
void insert2(int u,int v,int w){
	insert(u,v,w);
	insert(v,u,w);
}
int n,k;
int t[maxn],a[maxn];
int main(){
	//freopen("T2.txt","r",stdin);
	n=read();k=read();
	for(register int i=1;i<=n;i++)a[i]=read();
	int flag=n;
	while(flag--) {
		for(register int i=1;i<=n;i++){
			for(register int j=i+k;j<=n;j++) {
				if(fabs(a[i]-a[j])==1 && a[i]>a[j]) {
					int p=a[i];
					int q=a[j];
					a[i]=q;
					a[j]=p;
				}
			}
		}
	}
	for (int i=1;i<=n;i++)cout<<a[i]<<endl;
	return 0;
}

道理都懂,直接暴力查找就好了_(:з」∠)_
然后是知识分子的剪枝暴力,用的时间要稍微少一点,去掉了对于无法转换的字符的判断,虽然复杂度的上限依然很大,但是下限减小了不少↓

//40分
#include <bits/stdc++.h>
using namespace std;
long long n,k,p[500010],t[500010];
int main(){
	cin>>n>>k;
	for(int i=1;i<=n;i++){
		cin>>p[i];
		t[i] = p[i];
	}
	sort(t+1,t+1+n);
	bool flag = 1; 
	int ts =1;
	while(flag){
		flag = 0;
		for(int i=ts;i<n;i++){
			int  tk = 0;
			for(int j=i+1;j<=n;j++){
				if(abs(p[i] - p[j]) == 1){
					tk++;
				}
				if(abs(i - j) >= k && abs(p[i]-p[j])==1 && p[i]>p[j]){
					long long temp = p[i];
					p[i] = p[j];
					p[j] = temp;
					flag = 1;
					/*for(int k=1;k<=n;k++){	
						cout<<p[k]<<" ";
					}
					cout<<endl;*/
				}
				if(tk == 2){
					break;
				}
			}
		}
		if(p[ts] == t[ts]){
			ts++;
			//cout<<p[ts]<<endl;
		}
	//	t++;
		//cout<<t<<endl;
	}
	for(int i=1;i<=n;i++){
		cout<<p[i]<<endl;
	}
	return 0;
}

PS:我也不知道为什么会变成40分QAQ
最后是大佬的玄学暴力。每一次遍历的时候提前处理判断当前是否是最佳答案,对于每一对\(i\)\(j\)而言,若\(i\)\(j\)的前面,那么每一次判断性质时只判断$ i-j \(是否等于1而不是\)abs(i-j)\(,因为如果\)i-j=1\(成立就说明处在前方的数的值一定比处在后方的数的值要大,那么一定要执行交换操作。同理,如果\)j-i=1$就说明交换之后整个串的字典序要大于交换之前,因此不做交换。

#include <bits/stdc++.h>
#define maxn 500005
using namespace std;
//tql
inline char get(){
	static char buf[30],*p1=buf,*p2=buf;
	return p1==p2 && (p2=(p1=buf)+fread(buf,1,30,stdin),p1==p2)?EOF:*p1++;
}
inline int read(){
	register char c=get();register int f=1,_=0;
	while(c>'9' || c<'0')f=(c=='-')?-1:1,c=get();
	while(c<='9' && c>='0')_=(_<<3)+(_<<1)+(c^48),c=get();
	return _*f;
}
int n,m;
int a[maxn];
int main(){
	n=read(),m=read();
	for(int i=1;i<=n;i++)a[i]=read();
	bool swaped;
	for(register int k=1;k<=n && swaped;k++){
		swaped=false;
		for(register int i=1;i<=n-m;i++){
			for(register int j=i+m;j<=n;j++){
				if(a[i]-a[j]==1){
					swap(a[i],a[j]);
					swaped=true;
				}
			}
		}
	}
	for(int i=1;i<=n;i++)printf("%d\n",a[i]);
	return 0;
}

讲完暴力我们再来看下正解。(转,正解让本弱鸡根本无法理解,会在正解后给出自己的解法)
作者:a6219221
来源:CSDN
原文:https://blog.csdn.net/a6219221/article/details/52456053

  1. 从input的内容来分:有2种,分别是有duplicate和no duplicate的

解法:这两种情况的结果其实有有同一个特性:在同一个position,同一种字母只能出现一次。只不过没有duplicate的情况下不需要额外处理就可以达到这个效果。针对有duplicate的情况,我们在每次的recursion中都需要设置一个set来储存这个位置已经使用过的数字或者字母。见如下的tree:

example: input is: 1, 1, 2
on each recursion level, we skip the duplicate number
                /       |      \
               1       [1]重复  2               n
             /  \     / \      |  \
            1    2   1   2     1  [1]重复       n * (n - 1)
           /    /   /   /      |   |
          2    1   2   1       1   1            n * (n - 1) * (n - 2)
 
time: O(n!)
space: O(n)
1 1 2
1 2 1
2 1 1
  1. 从input的type来分:

  2. 可以是array

  3. 可以是list

  4. 也可以是个string

区别:这里的区别在于是否能够in place操作其中的element。比如array就可以快速的swap里面的element,这时候我们的permutation就可以使用swap的方式。但是string是没有办法这样操作的。所以如果后面2种情况出现又想要做in place的话,最好是重新建立一个array来储存其中的内容。这样也可以避免List的get()的时间复杂度不恒定的问题。string可以使用toCharArray()来得到char的array

  1. 从output的要求来分:

1是不用output,就print出来。

2是要返回一个List

3是要返回一个List<List>

区别: 1.如果不用output,我们可以写一个函数把那个array或者temp arraylist中的内容打印出来。这个问题一般不大

  1. 如果需要返回一个string,那么input一般来说是一个string或者是char array.这类的做法也比较方便,用swap的方法最后rst.add(new String(input))就可以实现

  2. 需要注意的是如果要求返回的值是List<List>,那么最后加入rst的那步一定是需要一个一个把array中的数字添加到最后的list中去的。最好是这样做,可以保证不会出问题。因为并不是很麻烦而且不会出现想用现成函数用错的情况。毕竟list中的是object而array中的可能是permitIve type

贴上代码
#include<bits/stdc++.h>
using namespace std;

const int maxn=5e5+10;

void read(int &x)
{
	char c=getchar(); x=0;
	while (c<'0'||c>'9') c=getchar();
	while (c>='0'&&c<='9') x=x*10+c-'0',c=getchar();
}

int n,k,p[maxn],q[maxn],deg[maxn];
int tote,FIR[maxn],TO[maxn<<1],NEXT[maxn<<1];
priority_queue<int> pq;

namespace SegTree{
	int mn[maxn<<2];
#define lc (nd<<1)
#define rc (nd<<1|1)
#define mid ((s+t)>>1)

	void init()
	{
		memset(mn,0x3f3f3f3f,sizeof(mn));
	}

	void update(int nd,int s,int t,int id,int val)
	{
		if (s==t) {mn[nd]=val; return;}
		if (id<=mid) update(lc,s,mid,id,val);
		else update(rc,mid+1,t,id,val);
		mn[nd]=min(mn[lc],mn[rc]);
	}

	int query(int nd,int s,int t,int l,int r)
	{
		if (l<=s&&t<=r) return mn[nd];
		int Ans=0x7fffffff;
		if (l<=mid) Ans=min(Ans,query(lc,s,mid,l,r));
		if (r> mid) Ans=min(Ans,query(rc,mid+1,t,l,r));
		return Ans;
	}
}

void addedge(int u,int v)
{
	TO[++tote]=v;
	NEXT[tote]=FIR[u];
	FIR[u]=tote;
	deg[v]++;
}

int main()
{
	int i,x;
	read(n); read(k);
	for (i=1;i<=n;i++)
		read(p[i]),q[p[i]]=i;
	SegTree::init();
	for (i=n;i>=1;i--)
	{
		x=SegTree::query(1,1,n,q[i]-k+1,q[i]);
		if (x<=n) addedge(q[x],q[i]);
		x=SegTree::query(1,1,n,q[i],q[i]+k-1);
		if (x<=n) addedge(q[x],q[i]);
		SegTree::update(1,1,n,q[i],i);
	}
	for (i=1;i<=n;i++)
		if (!deg[i]) pq.push(i);
	for (i=n;i>=1;i--)
	{
		int u=p[i]=pq.top(); pq.pop();
		for (int p=FIR[u];p;p=NEXT[p])
			if (!(--deg[TO[p]])) pq.push(TO[p]);
	}
	for (i=1;i<=n;i++) q[p[i]]=i;
	for (i=1;i<=n;i++) printf("%d\n",q[i]);
}
//转自一个ACM银牌选手

然后是本弱鸡的粉墨登场

首先我们要明确这是一个换位问题。这时我们假设有三个数\(a,b,c\),如果\(a\)\(b\)可以交换位置且\(b\)\(c\)可以换位置,那么我们就可以知道\(a\)一定可以和\(c\)换位置。因此我们需要一个值来存储可以换位置的点。用什么方法可以很容易的判断两个元素处于同一个集合里呢?很显然是并查集。我们初始化每次判断i和j位置的值,如果\(i\)\(j\)可以换位置,就在并查集中合并\(i\)\(j\),最后按字典序排列即可
这时我们就得到了一个看似正确的思路。事实上,这只正确了一半。因为当我们交换两个元素的位置后,他可以通往的位置会发生改变。因此这时我们需要再重复几次之前的步骤,直到最后所有元素都没法再交换位置得到最优解时再停止

#include<bits/stdc++.h>
#define maxn 500000
using namespace std;
inline char get(){
	static char buf[30],*p1=buf,*p2=buf;
	return p1==p2 && (p2=(p1=buf)+fread(buf,1,30,stdin),p1==p2)?EOF:*p1++;
}
inline int read(){
	register char c=get();register int f=1,_=0;
	while(c>'9' || c<'0')f=(c=='-')?-1:1,c=get();
	while(c<='9' && c>='0')_=(_<<3)+(_<<1)+(c^48),c=get();
	return _*f;
}
int fa[maxn];
int n,k;
void init(){
	for(register int i=0;i<=n;i++)fa[i]=i;
}
int get(int x){
	if(fa[x]==x)return x;
	return fa[x]=get(fa[x]);
}
int merge(int x,int y){
	x=get(x);
	y=get(y);
	if(x!=y)fa[y]=x;
}
struct edge{
	int a,p;
	//a记录当前的值,p记录位置 
}E[maxn];
bool cmp(edge a,edge b){
	return a.a<b.a;
}
bool outcmp(edge a,edge b){
	return a.p<b.p;
}
set<int> savenow;//存储每一次已经被用过的位置 
int cas[maxn];//记录上一次的排列顺序 
int main(){
	//freopen("T2.txt","r",stdin);
	init();
	n=read();k=read();
	for(register int i=1;i<=n;i++)E[i].a=read(),E[i].p=i;
	while(1){
		bool here=1;
		for(register int i=1;i<=n-k;i++){
			for(register int j=i+k;j<=n;j++){
				if(abs(E[i].a-E[j].a)==1)merge(i,j);
			}
		}
		for(register int i=1;i<=n;i++){
			int last_time=i;
			for(register int j=1;j<=n;j++){
				if(savenow.count(j))continue;
				if(get(E[i].p)==get(j)){
					if(E[i].p>E[j].p){
						swap(E[i].p,E[last_time].p);
						swap(E[i].p,E[j].p);
						last_time=j;
					}
				}
			}
			savenow.insert(E[i].p);
			if(cas[E[i].p]!=E[i].a){
				cas[E[i].p]=E[i].a;
				here=0;
			}
		}
		if(here)break;
	}
	for(register int i=0;i<=n;i++)printf("%d ",cas[i]);
	return 0;
}

是的,这看似是一个简便又快捷的写法,这时再让我们抱着激动的心情来计算一下复杂度
算了。溜(事实上这个算法的复杂度是\(O(n!)\)

posted @ 2018-10-20 17:34  迷失の风之旅人  阅读(203)  评论(0编辑  收藏  举报
123213123123