题意
设 pp 是一个质数。
你有一个 pk 个点的无向完全图(任意两个点之间有一条无向边),点的标号是 0 到 pk-1 。
现在你需要从中找出一些 p 个点的完全图,使得原图中每条边属于且恰好属于其中一个完全图。
很显然你需要找出的完全图的个数是 p k ( p k − 1 ) / 2 p ( p − 1 ) / 2 \frac{p^k(p^k- 1) / 2}{p(p-1)/2} p(p−1)/2pk(pk−1)/2 ,可以发现这个式子一定是整数。
输入格式
一行两个正整数 p,k。
输出格式
如果无解,输出一行 NO。
否则输出一行 YES,接下来输出 p k ( p k − 1 ) / 2 p ( p − 1 ) / 2 \frac{p^k(p^k- 1) / 2}{p(p-1)/2} p(p−1)/2pk(pk−1)/2 行,每行 p 个数表示你找出的这个完全图的点的编号。
按任意顺序输出任意一种方案即可。
样例
样例一:
input:
2 2
output:
YES
0 1
2 0
3 0
1 2
1 3
3 2
样例二:
input:
3 1
output:
YES
0 1 2
解题思路:
是否有解
题目要求我们判断能否利用
p
k
(
p
k
−
1
)
/
2
p
(
p
−
1
)
/
2
\frac{p^k(p^k- 1) / 2}{p(p-1)/2}
p(p−1)/2pk(pk−1)/2 个完全图,使得原完全图的每一条边都包含在这些完全图之中。
但,在这道题中,答案必定为YES
如果要使得原完全图内的每一条边都包含在这些完全图中,那么我们所构造的完全图必须任意两个点至少一次出现在同一行输出中
而达成以上条件需要的点对是多少呢?
答案是
(
p
k
−
1
)
+
(
p
k
−
2
)
+
.
.
.
+
2
+
1
(p^k- 1) + (p^k- 2)+ ... +2+1
(pk−1)+(pk−2)+...+2+1,即为一个公差为-1的等差数列,利用等差数列前n项和公式可求出,需要的点对为
p
k
(
p
k
−
1
)
/
2
p^k(p^k- 1) / 2
pk(pk−1)/2
而一个结点数为p的完全图所能够包含的点对为 C ( 2 p ) C{ 2 \choose p} C(p2),即 p ( p − 1 ) / 2 p(p-1)/2 p(p−1)/2。
可以发现,我们要将所有的点对表示出来所需要的完全图数量为 p k ( p k − 1 ) / 2 p ( p − 1 ) / 2 \frac{p^k(p^k- 1) / 2}{p(p-1)/2} p(p−1)/2pk(pk−1)/2,这与题目要求我们输出的完全图数量是相同的,也就是说,不管n与k为多少,均能恰好由 p k ( p k − 1 ) / 2 p ( p − 1 ) / 2 \frac{p^k(p^k- 1) / 2}{p(p-1)/2} p(p−1)/2pk(pk−1)/2 个结点为p的完全图表示出来。
递归输出
对于一个长度为pk的序列,要将这pk个元素形成包含所有点对的完全图,可将之划分为p行,每一行包含pk-1个元素,从每一行中取一个数,这p个数即可形成一个完全图,则这些不同的取法即可组成不同的完全图。
要从这个p*pk-1的矩阵中获得不同的取法,同时保证两个数有且只有一个完全图中出现,则可采用以下方式:第一行取走第 i 个元素,下一行中,则取走 i+t 个元素,再下一行取走 i + t * 2个元素,以此类推,当所有的元素都取完以后,t 增加,令 t 从0到p - 1遍历,即可将所有的元素都输出,举例如下:
设p = 3,k = 2,
则形成的矩阵如下:
0 1 2
3 4 5
6 7 8
1.首先,令 t = 0,起始下标 i 从0遍历至 pk-1-1,行标 j 从0遍历至p - 1,则形成的完全图有:
1 3 6
1 4 7
2 5 8
2.令 t = 1 , 起始坐标 i 与行标 j 如上,形成的完全图有:
0 4 8
1 5 6
2 3 7
3.令 t = 2,起始坐标 i 与行标 j 如上,形成的完全图有:
0 5 7
1 3 8
2 4 6
而后检测每一行的长度是否超过p,若每行的元素长度超过p,则需要将每一行当作一个新的长度为pk-1的序列,重复以上操作,而如果每一行的长度正好为 p,则每一行恰好能形成一个完全图, 将之输出如下:
0 1 2
3 4 5
6 7 8
当递归结束后,输出的行数将会恰好 p k ( p k − 1 ) / 2 p^k(p^k- 1) / 2 pk(pk−1)/2行,即为满足题目要求的完全图序列。
AC代码
#include<iostream>
#include<algorithm>
#include<stdio.h>
#include<math.h>
using namespace std;
int a[2005][2005];
void prin(int beg, int len, int p) {
//需要进行划分的数范围为 [beg,beg+len), 将所有数分为p行,每行len/p个数
int rowLen = len / p; //一行的数据个数
for (int i = 0; i < p; i++) {
for (int j = 0; j < rowLen; j++) {
a[i][j] = beg + i * rowLen + j;
}
}
for (int t = 0; t < rowLen; t++) { //t为输出时,每增加一行下标需要移动的位数
for (int i = 0; i < rowLen; i++) { //i为第一行的起始下标
for (int j = 0; j < p; j++) { // j为当前行数
// 由于每行下标移动t位,起始位置为i,则输出a[j][i+j*t],需要注意对一行数据的个数进行取余,避免超范围
printf("%d", a[j][(i + j * t) % rowLen]);
if (j < p - 1)
printf(" ");
else
printf("\n");
}
}
}
if (len > p * p) { // 当数据个数大于 p^2 ,说明还需要将每一行的数据进行递归,实现降幂效果
for (int i = 0; i < p; i++) {
prin(beg + i * rowLen, rowLen, p);//将一行的第一个元素,这一行的元素个数,p传入函数
//注:这里beg + i * rowLen 不能使用a[i][0]代替,因为调用函数时,会令全局数组a内的值改变
//当然也可以不让数组a成为全局数组,但这消耗的空间将会增加,所以没必要
}
}
else { // 否则,输出每一行数据,同时结束递归
for (int i = 0; i < p; i++) {
for (int j = 0; j < p; j++) {
printf("%d", a[i][j]);
if (j < p - 1)
printf(" ");
else
printf("\n");
}
}
}
return;
}
int main() {
int p, k;
scanf("%d %d", &p, &k);
int len = pow(p, k);
printf("YES\n"); //结果必为YES
if (k == 1) {//k = 1时,输出0到p-1
for (int i = 0; i < p; i++) {
printf("%d", i);
if (i < p - 1)
printf(" ");
else
printf("\n");
}
}
else {
prin(0, len, p);//k不为1,调用函数递归输出
}
return 0;
}