置换的概念还是比较好理解的,《组合数学》里面有讲。对于置换的幂运算大家可以参考一下潘震皓的那篇《置换群快速幂运算研究与探讨》,写的很好。
结论一:一个长度为l的循环T,l是k的倍数,则T^k是k个循环的乘积,每个循环分别是循环T中下标i mod k=0,1,2…的元素按顺序的连接。
结论二:一个长度为l的循环T,gcd(l,k)=1,则T^k是一个循环,与循环T不一定相同。
结论三:一个长度为l的循环T,T^k是m=gcd(l,k)个循环的乘积,每个循环分别是循环T中下标i mod gcd(l,k)=0,1,2…的元素的连接。
如果长度与指数不互质,单个循环就没有办法来开方。不过,我们可以选择相应m个长度相同的循环交错合并来完成开方的过程。可在这种情况下,如果找不到m个长度相同的循环,那就一定不能开方。其中:m是gcd(l,k)的倍数
*简单题:(应该理解概念就可以了)
# pku3270 Cow Sorting
题目描述:
给你一个数字序列(每个数字唯一),每次你可以交换任意两个数字,代价为这两个数字的和,问最少用多少代价能把这个序列按升序排列好。
题目的具体做法是参考刘汝佳的《算法艺术与信息学奥赛》大概思路是:以后再用别种方法解,
1.找出初始状态和目标状态。明显,目标状态就是排序后的状态。
2.画出置换群,在里面找循环。例如,数字是8 4 5 3 2 7
明显, 目标状态是2 3 4 5 7 8,能写为两个循环:(8 2 7)(4 3 5)。
3.观察其中一个循环,明显地,要使交换代价最小,应该用循环里面最小的数字2,去与另外的两个数字,7与8交换。这样交换的代价是:
sum - min + (len - 1) * min
化简后为:
sum + (len - 2) * min
其中,sum为这个循环所有数字的和,len为长度,min为这个环里面最小的数字。
4.考虑到另外一种情况,我们可以从别的循环里面调一个数字,进入这个循环之中,使交换代价更小。例如初始状态:1 8 9 7 6
可分解为两个循环:(1)(8 6 9 7),明显,第二个循环为(8 6 9 7),最小的数字为6。我们可以抽调整个数列最小的数字1进入这个循环。使第二个循环变为:(8 1 9 7)。让这个1完成任务后,再和6交换,让6重新回到循环之后。这样做的代价明显是:
sum + min + (len + 1) * smallest
其中,sum为这个循环所有数字的和,len为长度,min为这个环里面最小的数字,smallest是整个数列最小的数字。
5.因此,对一个循环的排序,其代价是sum - min + (len - 1) * min和sum + min + (len + 1) * smallest之中小的那个数字。但这里两个公式还不知道怎么推出来的。
6.我们在计算循环的时候,不需要记录这个循环的所有元素,只需要记录这个循环的最小的数及其和。
7.在储存数目的时候,我们可以使用一个hash结构,将元素及其位置对应起来,以达到知道元素,可以快速反查元素位置的目的。这样就不必要一个个去搜索。
#include < algorithm >
using namespace std;
int a[ 100002 ],b[ 100002 ],hash[ 100002 ];
int visit[ 100002 ];
int n;
__int64 ans,ans1,ans2,sum;
int main()
{
int i,len,min;
int start,id;
while (scanf( " %d " , & n) != EOF)
{
memset(visit, 0 , sizeof (visit));
for (i = 0 ;i < n;i ++ )
scanf( " %d " , & a[i]), b[i] = a[i];
sort(b,b + n);
for (i = 0 ;i < n;i ++ ) // 建立简单的hash,方便于知道元素反查其位置;
hash[b[i]] = i;
ans = 0 ;
for (i = 0 ;i < n;i ++ )
{
len = 0 ; sum = 0 ;
min = 0x7fffffff ;
start = a[i];
id = i;
if ( ! visit[i])
{
while ( 1 ) // 寻找置换群之中的循环;
{
sum += start; // 求总和;
visit[id] = 1 ;
if (min > start) // 记下最小的元素
min = start;
id = hash[start];
start = a[id]; // 反查元素;
len ++ ; // 求元素个数;
if (start == a[i]) // 表明找到一只置换群;
break ;
}
ans1 = sum - min + (len - 1 ) * min;
ans2 = sum + min + (len + 1 ) * b[ 0 ];
ans += ans1 < ans2 ? ans1:ans2;
}
}
printf( " %I64d\n " ,ans);
}
return 0 ;
}
# pku1026 Cipher //先找出所有置换循环,然后对于每一位来计算k%循环长度后对应于哪个位置,O(n)复杂度。注意读写方面的东西。
#include < string .h >
int main()
{
int i,n,j,k,t,len;
int pos[ 210 ],c[ 210 ]; // 初始数组。c是周期数组。
char s[ 210 ],ans[ 210 ]; // a接受字符、res是结果。
while (scanf( " %d " , & n),n)
{
for (i = 1 ;i <= n;i ++ )
scanf( " %d " , & pos[i]);
memset(c, 0 , sizeof (c));
for (i = 1 ;i <= n;i ++ )
{
j = i;
while (pos[j] != i) // 寻找周期。
j = pos[j], c[i] ++ ;
c[i] ++ ;
}
while (scanf( " %d " , & k),k)
{
getchar();
gets(s + 1 );
len = strlen(s + 1 );
while (len < n) // 填充字符串
s[ ++ len] = ' ' ;
for (i = 1 ;i <= n;i ++ ) // 模拟。
{
t = k % c[i]; // 由周期性,可以得到较少的模拟次数。
j = i;
while (t -- )
j = pos[j];
ans[j] = s[i];
}
ans[n + 1 ] = 0 ;
puts(ans + 1 );
}
puts( "" );
}
return 0 ;
}
*置换幂运算:
# pku1721 CARDS //详细见05集训队论文《置换群快速幂运算研究与探讨》。
很显然,这题的一副扑克牌就是一个置换,而每一次洗牌就是这个置换的平方运算。由于牌的数量是奇数,并且一开始是一个大循环,所以做平方运算时候不会分裂。所以,在任意时间,牌的顺序所表示的置换一定是一个大循环。
那么根据文章开头提到的定理:设T^k=e,(T为一循环,e为单位置换),那么k的最小正整数解为T的长度。
可以知道,这个循环的n次方是单位循环,换句话说,如果k mod n=1,那么这个循环的k次方,就是它本身。我们知道,每一次洗牌是一次简单的平方运算,洗x次就是原循环的2x次方。
因为n是奇数,2x mod n=1一定有一个 上面的算法是出题方给出的标准算法。显然,时间复杂度为O(n2+logs)。 换一个方向:给定了结果和s以后,可以简单地将这个目标置换用3.1节的方法开方s次得到结果。时间复杂度为O(n*s)。 或者可以更简单地,算出2s,将目标置换直接开2s次方。这里有一个技巧,因为在开方时只需要在循环中前进2s次,所以我们只关心(2s) mod n,也就免去了大数字的运算。所以,计算2s需要O(logs),而开方需要O(n)。整个时间复杂度为O(n+logs)。 # pku3128 Leonardo's Notebook 题目意思是:一个置换是否可以由另一个置换的平方得来的。一个置换的平方,原来偶数长的循环会被分裂成两段长度相等的循环,而奇数长的循环不会被分裂。题目只是问是否存在,所以只要看所给置换中偶数长的循环是否成对,否则就不能由一个置换的平方得来。 补充:因为如果所给置换的循环是偶数,则肯定是由分裂过来的,那么一定是成对的,否则如果是奇数,那么有可能是原来是奇数,也有可能是原来的偶数分裂成两个奇数循环。 *推荐:(不错的应用) # pku3590 The shuffle Problem //把n分解成若干个数,使得他们的lcm最大。在所取的数都是素数幂的时候是最大的,所以可以用递归来枚举所有的分解情况,而且由于要输出序最小的,所以对于剩下的数可以直接单独都作为一个循环,这样就可以使得序最小了。此外,这道题目需要注意求最大的lcm的时候不能用dp来做,因为这个具有后效性,局部最优不一定使得全局最优。
using
namespace
std;
const
int
MAX
=
1001
;
int
a[MAX],b[MAX];
int
main()
{
int
i,j,k,n,s;
while
(scanf(
"
%d%d
"
,
&
n,
&
s)
!=
EOF)
{
for
(i
=
1
;i
<=
n;i
++
)
scanf(
"
%d
"
,
&
a[i]);
//
求目标循环结果存放在数组b[]中;
b[
1
]
=
1
;
i
=
j
=
1
;
while
(a[j]
!=
1
)
{
j
=
a[j];
b[
++
i]
=
j;
}
k
=
1
;
//
求位移的步数
for
(i
=
1
;i
<=
s;i
++
)
k
=
(k
*
2
)
%
n;
//
求开k次方运算,开方运算后的原始循环存放在数组a[]中;
a[
1
]
=
b[
1
];
j
=
1
;
for
(i
=
2
;i
<=
n;i
++
)
{
j
+=
k;
if
(j
>
n) j
-=
n;
a[j]
=
b[i];
}
//
原始循环转化为原始置换
for
(i
=
1
;i
<
n;i
++
)
b[a[i]]
=
a[i
+
1
];
b[a[n]]
=
a[
1
];
for
(i
=
1
;i
<=
n;i
++
)
printf(
"
%d\n
"
,b[i]);
}
return
0
;
}
#include
<
string
.h
>
char
s[
27
];
int
a[
27
],f[
27
],c[
27
];
int
ok()
{
int
i;
for
(i
=
0
;i
<
26
;i
++
)
if
(f[i])
{
int
cnt
=
1
;
int
b
=
a[i];
f[i]
=
0
;
while
(b
!=
i)
{
f[b]
=
0
;
b
=
a[b];
cnt
++
;
}
c[cnt]
++
;
}
for
(i
=
2
;i
<
27
;i
+=
2
)
if
(c[i]
%
2
)
return
0
;
return
1
;
}
int
main()
{
int
i,t;
scanf(
"
%d
"
,
&
t);
while
(t
--
)
{
scanf(
"
%s
"
,s);
for
(i
=
0
;i
<
26
;i
++
)
{
a[i]
=
s[i]
-
'
A
'
;
f[i]
=
1
; c[i]
=
0
;
}
puts(ok()
?
"
Yes
"
:
"
No
"
);
}
return
0
;
}
#include
<
algorithm
>
using
namespace
std;
const
int
N
=
105
;
bool
hash[N];
int
p[N],lp;
void
prim()
{
memset(hash,
true
,
sizeof
(hash));
lp
=
0
;
for
(
int
i
=
2
;i
<
N;i
++
)
{
if
(hash[i])
{
p[lp
++
]
=
i;
for
(
int
j
=
i
*
i;j
<
N;j
+=
i)
hash[j]
=
false
;
}
}
}
int
step[N],maxm,cycle[N],lc;
void
dfs(
int
remain,
int
k)
{
if
(remain
<
p[k])
{
int
m
=
1
;
for
(
int
i
=
0
;i
<
k;i
++
)
if
(step[i])
m
*=
step[i];
if
(m
>
maxm)
{
maxm
=
m;
lc
=
0
;
for
(
int
i
=
0
;i
<
k;i
++
)
if
(step[i])
cycle[lc
++
]
=
step[i];
while
(remain
--
)
cycle[lc
++
]
=
1
;
}
}
else
{
step[k]
=
0
;
dfs(remain,k
+
1
);
for
(step[k]
=
p[k];step[k]
<=
remain;step[k]
*=
p[k])
dfs(remain
-
step[k],k
+
1
);
}
}
int
main()
{
int
t,n;
prim();
scanf(
"
%d
"
,
&
t);
while
(t
--
){
scanf(
"
%d
"
,
&
n);
if
(n
==
1
)
{
printf(
"
1 1\n
"
);
continue
;
}
maxm
=
1
; lc
=
0
;
dfs(n,
0
);
sort(cycle,cycle
+
lc);
printf(
"
%d
"
,maxm);
int
k
=
1
,tmp;
for
(
int
i
=
0
;i
<
lc;i
++
)
{
tmp
=
k
++
;
for
(
int
j
=
1
;j
<
cycle[i];j
++
)
printf(
"
%d
"
,k
++
);
printf(
"
%d
"
,tmp);
}
puts(
""
);
}
return
0
;
}