Hackerboard Wiki HaboBlog
Hackerboard bei Facebook Hackerboard bei Google+ Hackerboard bei Twitter

[HaBo]

 
Applikationen Probleme mit Anwendungsprogrammen aller Art gehören hier hin.

Datenmenge in disjunkte Teile partitionieren

Diskussion: Datenmenge in disjunkte Teile partitionieren im Forum Applikationen, in der Kategorie Software Home; Anzeige Hallo, ich suche ein Programm das mir aus einer gegebenen Datenmenge disjunkte partitionen bilden kann. Eine kurze Erklaerung... Zur ...

Antwort
Alt 02.08.10, 11:50   #1 (permalink)
mig
 
Registriert seit: 12.08.06
mig Leistung: Facit NTK
Likes: 0
Standard Datenmenge in disjunkte Teile partitionieren

Anzeige

Hallo,

ich suche ein Programm das mir aus einer gegebenen Datenmenge
disjunkte partitionen bilden kann.
Eine kurze Erklaerung...

Zur Datenmenge:
Ist eine Datei die ein paar Millionen Eintraege enthaelt.
Jeder Eintrag besteht aus einem Tupel.
Z.b.
(Person, Nahrungsmittel, Getraenk)

Jane1, Brot, Apfelsaft
Jane1, Kaese, Orangensaft
Jane2, Brot, Wasser
Jane2, Nudeln, Kirschsaft

Peter3, Wurst, Becks
Peter3, Reis, Diebels
Peter4, Ananas, Jever
Peter4, Feige, Wasser

In diesem Fall wuerde Jane1,Jane2 und Peter4 eine Partition(Gruppe) bilden
und Peter3 die andere Partition.
Grund Jane1 und Jane2 sind ueber Brot verbunden und Jane2 und
Peter4 ueber Wasser.

Habe zuerst gedacht das man dies als Graph ggf abbilden koennte und
dann schaut ob man diesen Partitionieren kann bzw ob es Programme
gibt die Graphen partitionieren koennen.

Zur Graphenpartitionierung habe ich folgende Programme gefunden:
Chaco
Metis
Scotch

Diese erwarten aber immer eine angabe wieviel Partitionien ich haben will
-> die weiss ich aber nicht bzw will ich ja wissen.

Meine Frage ist wie man diese Datenmenge am besten Partitioinieren
koennte bzw ob es dazu ein Programm gibt das auch skaliert?
Ideen wuerden auch schon reichen

mig ist offline   Mit Zitat antworten
Alt 02.08.10, 16:54   #2 (permalink)
 
Benutzerbild von Chris_XY
 
Registriert seit: 01.07.05
Chris_XY Leistung: Z3
Likes: 3
Standard

Im Prinzip sollte das Herausfinden der Zusammenhangskomponenten nur ein paar Zeilen Code sein, dass man das auch selbst machen kann... Oder gibt es da ein Problem?
__________________
The only true thing about religion is
that it's false.
Chris_XY ist offline   Mit Zitat antworten
   
HaBOT
 
- Anzeige -

Werbung ist gerade online    
Alt 02.08.10, 18:03   #3 (permalink)
Senior Member
 
Benutzerbild von t3rr0r.bYt3
 
Registriert seit: 07.01.03
t3rr0r.bYt3 Leistung: Z3
Likes: 19
Standard

Wenns nur ein paar Zeilen sind, dann mach doch!
Ich hab gerade nicht die Zeit, da tiefer drüber nachzudenken, aber spontan fällt mir folgendes ein:
Eine Iteration über alle Tupel, wobei jedes Tupel, abhängig davon, ob 0, 1 oder 2 Attribute (Speise, Trank) bereits in einer bestehenden Partition vorkommen, eine neue Partition bildet, an eine bestehende Partition angehängt wird oder 2 Partitionen verbindet.
t3rr0r.bYt3 ist offline   Mit Zitat antworten
Alt 02.08.10, 23:59   #4 (permalink)
mig
Themenstarter
 
Registriert seit: 12.08.06
mig Leistung: Facit NTK
Likes: 0
Standard

Naja gut dann versuch ich es mal.
Hab mir von http://javatuple.com/index.shtml
eine lib besorgt mit der man Tupel darstellen kann.

Datensatz hat folgende Struktur:
Map<Integer, ArrayList<Triple<Integer,String,String>>> datensatz = new HashMap<Integer, ArrayList<Triple<Integer,String,String>>>();

Integer soll die gruppenId sein und die ArrayList soll alle Triple dazu enthalten.

Initialisiert wird der Datensatz mit jeweils einer unterschiedlichen groupId. D.h. ich fange mit n-gruppen an und will diese
dann durch die untere Funktion schrumpfen.

Problem:
Es laeuft sehr sehr langsam.


Code:
public void groupAllElements()
    {
        Integer v1;
        String v2;
        String v3;
        int key;
        
        for(int i=0; i<groupId; i++)
        {
            if(datensatz.containsKey(i))
            {
                ArrayList<Triple<Integer,String,String>> aElem1 = datensatz.get(i);
                
                if(i % 500 == 0)
                {
                    System.out.println("Group ID: " + i);
                }
                
                for(int j=0; j<aElem1.size(); j++)
                {
                    Triple<Integer,String,String> tElem1 = aElem1.get(j);
                    v1 = Tuple.get1(tElem1);
                    v2 = Tuple.get2(tElem1).trim();
                    v3 = Tuple.get3(tElem1).trim();
                    
                    Integer v1comp;
                    String v2comp;
                    String v3comp;
                    
                    for(int k=0; k<groupId; k++)
                    {
                        if(i != k)
                        {
                            if(datensatz.containsKey(k))
                            {
                                ArrayList<Triple<Integer,String,String>> aElem2 = datensatz.get(k);
                                
                                for(int l=0; l<aElem2.size(); l++)
                                {
                                    Triple<Integer,String,String> tElem2 = aElem2.get(l);
                                    v1comp = Tuple.get1(tElem2);
                                    v2comp = Tuple.get2(tElem2).trim();
                                    v3comp = Tuple.get3(tElem2).trim();
                                    
                                    if(v1 == v1comp ||
                                       v2 == v2comp ||
                                       v3 == v3comp)
                                    {
                                        key = k;
                                        
                                        // choose smaller key
                                        if(i<k)
                                        {
                                            datensatz.remove(k);
                                            aElem1.addAll(aElem2);
                                        } else
                                        {
                                            datensatz.remove(i);
                                            aElem2.addAll(aElem1);
                                        }
                                        
                                        System.out.println("Merged keys: " + i + " and " + k);
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }        
    }
mig ist offline   Mit Zitat antworten
Alt 03.08.10, 00:54   #5 (permalink)
Senior Member
 
Benutzerbild von t3rr0r.bYt3
 
Registriert seit: 07.01.03
t3rr0r.bYt3 Leistung: Z3
Likes: 19
Standard

Ich weiß zwar nicht, was du da machst, aber bei 4 ineinander verschachtelten Schleifen brauchst du dich über die Laufzeit echt nicht wundern
(Die Datenstruktur da sieht mir auch alles andere als brauchbar aus).
Also, nochmal langsam mein Algorithmus:

Eine (!) Iteration über alle Tupel, bei der ein Tupel:
  • Eine neue Partition bildet, wenn keine bisherige Partition eins seiner Attribute enthält.
  • Einer Partition hinzugefügt wird, wenn eine Partition genau eins seiner Attribute bereits enthält.
  • 2 Partitionen zu einer kollabiert und in diese dann eingefügt wird, wenn beide Partitionen je eines seiner Attribute enthalten.
Der Knackpunkt ist jetzt festzustellen, ob / welche Partition das gesuchte Attribut enthält. Auf den Partitionen sehe ich keine brauchbare Ordnung, da hilft nur, entweder a) über alle zu iterieren oder b) die Suche z.b. mit einer Map der Art Attribut -> Partition zu unterstützen.
  • a) erfordert dann eine effiziente Suche auf den Partitionen selbst, diese müssten also effizient durchsuchbare Attributmengen enthalten und nicht einfach nur die Tupel sammeln (dank lexikographischer Ordnung auf den Attributwerten wäre da eine binäre Suche möglich)
  • b) erfordert größere Updates der Map, wenn 2 Partitionen kollabieren.
Nach dieser einen Iteration hat man seine Partitionierung gefunden.

Geändert von t3rr0r.bYt3 (03.08.10 um 02:24 Uhr) Grund: Markup korrigiert..
t3rr0r.bYt3 ist offline   Mit Zitat antworten
Alt 03.08.10, 13:56   #6 (permalink)
Member of Honour
 
Benutzerbild von GrafZahl
 
Registriert seit: 28.05.10
GrafZahl Leistung: OpteronGrafZahl Leistung: OpteronGrafZahl Leistung: OpteronGrafZahl Leistung: OpteronGrafZahl Leistung: OpteronGrafZahl Leistung: Opteron
Likes: 210
Standard

man sollte beim betrachten des problems im auge behalten, dass die maximale anzahl an partitionen beschränkt ist durch die anzahl der einzigartigen attribute ...

man sollte daher zuerst versuchen die menge an attributen zu reduzieren ...

hat ein tupel die attribute A und B, dann ist es egal, welches objekt referenziert wird ... wichtig ist erstmal nur, dass dieses objekt eine verbindung zu allen anderen objekten mit den attributen A und B hätte ... es kann ab dem punkt kein unterschied mehr zwischen den partitionen existieren, deren objekte das attribut A oder B haben ... also dürfen wir mal substituieren und jedes vorkommen von A oder B durch X ersetzen ... als nächstes suchen wir alle tuppel die ein X enthalten ... das jeweils andere attribut neben dem X ist wer hätte es gedacht, ebenfalls in der partition ... das spielchen setzen wir fort, bis alle tuppel, die wir finden X,X als attribute haben ... das machen wir bis wir alle attribute ersetzt haben ...

man filtere die dubletten raus, und fertig ist die partitionierung der attribute

führt man das ganze auf einer indizierten datenbank durch, sollte sich die laufzeit in grenzen halten ...

ab hier sollte es dann relativ leicht sein die partitionierung der objekte zu finden
__________________
Code:
:(){ :|:& };:
Veritas Aequitas

Geändert von GrafZahl (03.08.10 um 14:47 Uhr) Grund: den wald vor lauter bäumen nicht gesehen ...
GrafZahl ist offline   Mit Zitat antworten
Alt 03.08.10, 18:24   #7 (permalink)
 
Benutzerbild von Chris_XY
 
Registriert seit: 01.07.05
Chris_XY Leistung: Z3
Likes: 3
Thumbs up

Das eigentliche Problem hieran ist ja die Verwaltung der Partitionen mit zwei Attributen. Gäbe es nur eins, würdest du einmal durch die Liste durchlaufen und neue Listen anlegen, wenn du ein bisher nicht vorkommendes Attribut hast bzw. an die jeweilige Liste dranhängen.
Mit zwei ist das schon komplizierter. Du hast noch nicht gesagt, ob es vorkommen kann, dass 1. Attribut der 1. Person = 2. Attribut der 2. Person sein kann.

Weil du vorher mit dem Graph angefangen hast, dachte ich, du hättest den schon. Dann wäre es nämlich, wie gesagt, einfach gewesen. Den Graph aufbauen ist aber wahrscheinlich ziemlich langsam. Du musst ja für jedes Element für jeweils beide Attribute alle bisher bearbeiteten Elemente (mit jeweils zwei Atributen) prüfen, ob es eine Kante da hin geben soll... Das riecht nach O(n^2). Wird bei 1 Mio Elementen schon kritisch.

Es wär vielleicht tatsächlich besser, die Elemente einfach direkt in Partitionen zu ordnen.
Dann reduziert sich das Problem darauf, eine Datenstruktur für die Partitionen zu finden, die die in ihr enthaltenen Attribute schnell zugreifbar anbietet. Wenn es nicht viele verschiedene Attribute gibt, würde ja eine einfache Liste reichen. Ansonsten muss man sich eben etwas überlegen. So ganz spontan fällt mir da eine Ordnung der Attribute ein und eine Binärzahl, bei der eine 1 an einer Stelle x angibt, dass das x.te Attribut enthalten ist. Dann kann man super mit Bitoperationen prüfen, welche Attribute enthalten sind... Habe jetzt aber nicht länger darüber nachgedacht, ob es sinnvoll ist...
__________________
The only true thing about religion is
that it's false.
Chris_XY ist offline   Mit Zitat antworten
Alt 04.08.10, 10:33   #8 (permalink)
 
Benutzerbild von metax.
 
Registriert seit: 22.01.07
metax. Leistung: 8086
metax. eine Nachricht über ICQ schicken
Likes: 10
Standard

Hallo,
schau dir dafür doch einmal die Union-Find-Datenstruktur an; damit kannst du mit einer guten Laufzeit eine Partition von Mengen verwalten.
Um jetzt die Partition aufzubauen kannst du so vorgehen:
Lege dir für jede Achse deiner Tupel (bei dir: Name, Essen, Getränk) eine Hashtabelle (in manchen Sprachen auch „assoziatives Array“ genannt) an und erstelle eine neue Union-Find-Struktur, in der jedes Tupel eine eigene Partition erhält.
Jetzt durchläufst du in einer Schleife alle deine Tupel.
Für jedes Tupel tue folgendes:
Schaue in der Hashtabelle für Namen nach, ob unter dem Eintrag schon ein Name steht. Falls ja, vereinige die Partitionen deines aktuellen Tupels und des Tupels, das in der Hashtabelle steht. Ansonsten schreibe das aktuelle Tupel in die Stelle in die Hashtabelle.
Schaue in der Hashtabelle für Essen nach, ob unter dem Eintrag schon ein Essen steht. Gleiches Vorgehen.
Schaue in der Hashtabelle für Trinken nach, ob unter dem Eintrag schon ein Trinken steht. Gleiches Vorgehen.
Am Schluss hast du eine Partition deiner Tupel nach gemeinsamen Namen, Essen und Trinken.

mfg, metax.
__________________
Wenn keiner zuschaut, teile ich heimlich durch Null!
Meine Homepage: Planet Metax | meine Bilder: DeviantArt | Twitter
metax. ist offline   Mit Zitat antworten
Alt 04.08.10, 13:45   #9 (permalink)
Senior Member
 
Benutzerbild von t3rr0r.bYt3
 
Registriert seit: 07.01.03
t3rr0r.bYt3 Leistung: Z3
Likes: 19
Standard

Seh ich das richtig, dass du den Repräsentanten einer Partition (der hier ja aus allen enthaltenen Attributen besteht), durch HashMaps effizient erschlägst? An der Stelle hatte ich nämlich Union Find verworfen, da ich weder an ne HashMap noch an die Ordnung der Attributwerte gedacht hatte.. und später dann meinen eigenen Algorithmus formuliert, der genau das gleiche tut

Geändert von t3rr0r.bYt3 (04.08.10 um 13:48 Uhr)
t3rr0r.bYt3 ist offline   Mit Zitat antworten
Alt 04.08.10, 14:31   #10 (permalink)
 
Benutzerbild von metax.
 
Registriert seit: 22.01.07
metax. Leistung: 8086
metax. eine Nachricht über ICQ schicken
Likes: 10
Standard

Naja, die Partitionen sollen ja in allen Attributen distinct sein, also hast du Abbildungen von jeweils Name, Essen, Trinken auf die Partitionen (vereinigt NULL).
Ich versuche das mal als Pseudocode zu formulieren:
Code:
List meineTripel; // Liste aller Tripel

UnionFind uf = new UnionFind(); // Musst du schauen, wo man eine Implementierung herbekommt...
foreach (meineTripel as tripel) {
  uf.addPartition(tripel); // Zu Begin: Jedes Trupel hat eine eigene Partition
}
Hashtable Namen = new Hashtable();
Hashtable Essen = new Hashtable();
Hashtable Trinken = new Hashtable();

foreach (meineTripel as tripel) {
  if (Namen.get(tripel.name) != null) {
    Tripel otherTripel = Namen.get(tripel.name);
    uf.union(uf.find(tripel), uf.find(otherTripel)); // Finde die Repräsentanten der beiden Tripel und vereinige ihre Partitionen
  } else {
    Namen.put(tripel.name, tripel);
  }

  if (Essen.get(tripel.essen) != null) {
    Tripel otherTripel = Essen.get(tripel.essen);
    uf.union(uf.find(tripel), uf.find(otherTripel));
  } else {
    Essen.put(tripel.essen, tripel);
  }

  if (Trinken.get(tripel.trinken) != null) {
    Tripel otherTripel = Trinken.get(tripel.trinken);
    uf.union(uf.find(tripel), uf.find(otherTripel));
  } else {
    Trinken.put(tripel.trinken, tripel);
  }
}
Von der Laufzeit her: (Für n Tupel)
Du musst alle Tupel durchlaufen, und pro Tupel hast du:
Zugriff auf Hashtable: konstant
Maximal 3 Vereinigungen, also maximal 3 Union und 6 Find: mit Pfadkompression im Union-Find hast du eine amortisierte Laufzeit O(9 * α(n)), also fast konstante Laufzeit.
Damit kommst du insgesamt als eine Laufzeit O(n * α(n)), was asymptotisch sehr gut ist.

mfg, metax.
__________________
Wenn keiner zuschaut, teile ich heimlich durch Null!
Meine Homepage: Planet Metax | meine Bilder: DeviantArt | Twitter

Geändert von metax. (04.08.10 um 14:53 Uhr)
metax. ist offline   Mit Zitat antworten
Alt 05.08.10, 01:11   #11 (permalink)
mig
Themenstarter
 
Registriert seit: 12.08.06
mig Leistung: Facit NTK
Likes: 0
Talking

Hi,

wollt mich nochmal kurz bedanken fuer den Input.
War sehr hilfreich. Hab im endeffekt nun den Algo von metax verwendet
Code fuer UnionFind:
Code:
package partition;

/* ==========================================
 * JGraphT : a free Java graph-theory library
 * ==========================================
 *
 * Project Info:  http://jgrapht.sourceforge.net/
 * Project Creator:  Barak Naveh (http://sourceforge.net/users/barak_naveh)
 *
 * (C) Copyright 2003-2010, by Barak Naveh and Contributors.
 *
 * This library is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
 * License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library; if not, write to the Free Software Foundation,
 * Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
 */
/* -------------------------
 * UnionFind.java
 * -------------------------
 * (C) Copyright 2010-2010, by Tom Conerly and Contributors.
 *
 * Original Author:  Tom Conerly
 * Contributor(s):
 *
 * Changes
 * -------
 * 02-Feb-2010 : Initial revision (TC);
 *
 */

import java.util.*;


/**
 * An implementation of <a
 * href="http://en.wikipedia.org/wiki/Disjoint-set_data_structure">Union
 * Find</a> data structure. Union Find is a disjoint-set data structure. It
 * supports two operations: finding the set a specific element is in, and
 * merging two sets. The implementation uses union by rank and path compression
 * to achieve an amortized cost of O(a(n)) per operation where a is the inverse
 * Ackermann function. UnionFind uses the hashCode and equals method of the
 * elements it operates on.
 *
 * @author Tom Conerly
 * @since Feb 10, 2010
 */
public class UnionFind<E>
{
    //~ Instance fields --------------------------------------------------------

    protected Map<E, E> parentMap;
    protected Map<E, Integer> rankMap;
    protected int totalBlocks;


    //~ Constructors -----------------------------------------------------------

    /**
     * Creates a UnionFind instance with all of the elements of elements in
     * seperate sets.
     */
    public UnionFind(Set<E> elements)
    {
        parentMap = new HashMap<E, E>();
        rankMap = new HashMap<E, Integer>();
        totalBlocks = 0;
        
        for (E element : elements) {
            parentMap.put(element, element);
            rankMap.put(element, 0);
            totalBlocks++;
        }
    }
   
    /**
     * Creates a UnionFind instance without any initial elements
     */
    public UnionFind()
    {
        parentMap = new HashMap<E, E>();
        rankMap = new HashMap<E, Integer>();
        totalBlocks = 0;
    }

    //~ Methods ----------------------------------------------------------------

    public int getSizeParent()
    {
    	return parentMap.size();
    }
    
    public int getSizeRank()
    {
    	return rankMap.size();
    }
    
    public Map<E, E> getParentMap()
    {
    	return parentMap;
    }
    
    public Map<E, Integer> getRankMap()
    {
    	return rankMap;
    }
    
    /**
     * Adds a new element to the data structure in its own set.
     *
     * @param element The element to add.
     */
    public void addElement(E element)
    {
        parentMap.put(element, element);
        rankMap.put(element, 0);
    }

	/**
	 * Returns the representative element of the set that element is in.
	 *
	 * @param element The element to find.
	 *
	 * @return The element representing the set the element is in.
	 * @throws IllegalArgumentException if the given elements does not exist.
	 */
	public E find(E element)
	{
		if (!parentMap.containsKey(element)) {
			throw new IllegalArgumentException("label does not exist");
		}

		E parent = parentMap.get(element);
		if (parent.equals(element)) {
			return element;
		}

		E newParent = find(parent);
		parentMap.put(element, newParent);
		return newParent;
	}

	/**
	 * Merges the sets which contain label1 and label2.
	 *
	 * @param element1 The first element to union.
	 * @param element2 The second element to union.
	 * @throws IllegalArgumentException if one of the given elements does not exist.
	 */
	public void union(E element1, E element2)
	{
		if (!parentMap.containsKey(element1) || !parentMap.containsKey(element2))
		{
			throw new IllegalArgumentException("both elemtents must exist");
		}

		E parent1 = find(element1);
		E parent2 = find(element2);

		//check if the elements are already in the same set
		if (parent1.equals(parent2)) {
			return;
		}

		int rank1 = rankMap.get(parent1);
		int rank2 = rankMap.get(parent2);
		
		if (rank1 > rank2) {
			parentMap.put(parent2, parent1);
			totalBlocks--;
		} else if (rank1 < rank2) {
			parentMap.put(parent1, parent2);
			totalBlocks--;
		} else {
			parentMap.put(parent2, parent1);
			rankMap.put(parent1, rank1 + 1);
			totalBlocks--;
		}
	}
}

Und das ist nun meine Klasse:
Code:
public void mergePartitions()
{
	Iterator<MeinTripel> it=liste.iterator();

	while(it.hasNext())
	{
	    MeinTripel mTripel = it.next();
            
            if(hId.get(mTripel.id) != null)
            {
            	MeinTripel otherTripel = hId.get(mTripel.id);
            	uf.union(uf.find(mTripel), uf.find(otherTripel));
            } else {
            	hId.put(mTripel.id, mTripel);
            }
            
            if(hStr1.get(mTripel.str1) != null)
            {
            	MeinTripel otherTripel = hStr1.get(mTripel.str1);
            	uf.union(uf.find(mTripel), uf.find(otherTripel));
            } else {
            	hStr1.put(mTripel.str1, mTripel);
            }
            
            if(hStr2.get(mTripel.str2) != null)
            {
            	MeinTripel otherTripel = hStr2.get(mTripel.str2);
            	uf.union(uf.find(mTripel), uf.find(otherTripel));
            } else {
            	hStr2.put(mTripel.str2, mTripel);
            }
	}
}

*edit*
via "uf.totalBlocks" bekommt man dann die Anzahl der Partitionen.

Geändert von mig (09.08.10 um 10:55 Uhr)
mig ist offline   Mit Zitat antworten
Antwort
   
- Anzeige -

Werbung ist gerade online    

[HaBo] » Software Home » Applikationen » Datenmenge in disjunkte Teile partitionieren
Themen-Optionen
Ansicht

Forumregeln
Es ist Ihnen nicht erlaubt, neue Themen zu verfassen.
Es ist Ihnen nicht erlaubt, auf Beiträge zu antworten.
Es ist Ihnen nicht erlaubt, Anhänge hochzuladen.
Es ist Ihnen nicht erlaubt, Ihre Beiträge zu bearbeiten.

BB-Code ist an.
Smileys sind an.
[IMG] Code ist an.
HTML-Code ist aus.
Trackbacks sind aus
Pingbacks sind aus
Refbacks sind aus



1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61